initial
This commit is contained in:
16
env/lib/python3.11/site-packages/pymodbus/__init__.py
vendored
Normal file
16
env/lib/python3.11/site-packages/pymodbus/__init__.py
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
"""Pymodbus: Modbus Protocol Implementation.
|
||||
|
||||
Released under the BSD license
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
"pymodbus_apply_logging_config",
|
||||
"__version__",
|
||||
"__version_full__",
|
||||
]
|
||||
|
||||
from pymodbus.logging import pymodbus_apply_logging_config
|
||||
|
||||
|
||||
__version__ = "3.5.4"
|
||||
__version_full__ = f"[pymodbus, version {__version__}]"
|
||||
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/bit_read_message.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/bit_read_message.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/bit_write_message.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/bit_write_message.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/constants.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/constants.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/device.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/device.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/diag_message.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/diag_message.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/events.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/events.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/exceptions.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/exceptions.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/factory.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/factory.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/file_message.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/file_message.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/logging.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/logging.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/mei_message.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/mei_message.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/other_message.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/other_message.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/payload.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/payload.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/pdu.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/pdu.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/register_read_message.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/register_read_message.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/register_write_message.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/register_write_message.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/transaction.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/transaction.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/utilities.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/__pycache__/utilities.cpython-311.pyc
vendored
Normal file
Binary file not shown.
270
env/lib/python3.11/site-packages/pymodbus/bit_read_message.py
vendored
Normal file
270
env/lib/python3.11/site-packages/pymodbus/bit_read_message.py
vendored
Normal file
@@ -0,0 +1,270 @@
|
||||
"""Bit Reading Request/Response messages."""
|
||||
|
||||
__all__ = [
|
||||
"ReadBitsResponseBase",
|
||||
"ReadCoilsRequest",
|
||||
"ReadCoilsResponse",
|
||||
"ReadDiscreteInputsRequest",
|
||||
"ReadDiscreteInputsResponse",
|
||||
]
|
||||
|
||||
# pylint: disable=missing-type-doc
|
||||
import struct
|
||||
|
||||
from pymodbus.pdu import ExceptionResponse, ModbusRequest, ModbusResponse
|
||||
from pymodbus.pdu import ModbusExceptions as merror
|
||||
from pymodbus.utilities import pack_bitstring, unpack_bitstring
|
||||
|
||||
|
||||
class ReadBitsRequestBase(ModbusRequest):
|
||||
"""Base class for Messages Requesting bit values."""
|
||||
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address, count, slave=0, **kwargs):
|
||||
"""Initialize the read request data.
|
||||
|
||||
:param address: The start address to read from
|
||||
:param count: The number of bits after "address" to read
|
||||
:param slave: Modbus slave slave ID
|
||||
"""
|
||||
ModbusRequest.__init__(self, slave, **kwargs)
|
||||
self.address = address
|
||||
self.count = count
|
||||
|
||||
def encode(self):
|
||||
"""Encode a request pdu.
|
||||
|
||||
:returns: The encoded pdu
|
||||
"""
|
||||
return struct.pack(">HH", self.address, self.count)
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode a request pdu.
|
||||
|
||||
:param data: The packet data to decode
|
||||
"""
|
||||
self.address, self.count = struct.unpack(">HH", data)
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""Get response pdu size.
|
||||
|
||||
Func_code (1 byte) + Byte Count(1 byte) + Quantity of Coils (n Bytes)/8,
|
||||
if the remainder is different of 0 then N = N+1
|
||||
:return:
|
||||
"""
|
||||
count = self.count // 8
|
||||
if self.count % 8:
|
||||
count += 1
|
||||
|
||||
return 1 + 1 + count
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of the instance.
|
||||
|
||||
:returns: A string representation of the instance
|
||||
"""
|
||||
return f"ReadBitRequest({self.address},{self.count})"
|
||||
|
||||
|
||||
class ReadBitsResponseBase(ModbusResponse):
|
||||
"""Base class for Messages responding to bit-reading values.
|
||||
|
||||
The requested bits can be found in the .bits list.
|
||||
"""
|
||||
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, values, slave=0, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param values: The requested values to be returned
|
||||
:param slave: Modbus slave slave ID
|
||||
"""
|
||||
ModbusResponse.__init__(self, slave, **kwargs)
|
||||
|
||||
#: A list of booleans representing bit values
|
||||
self.bits = values or []
|
||||
|
||||
def encode(self):
|
||||
"""Encode response pdu.
|
||||
|
||||
:returns: The encoded packet message
|
||||
"""
|
||||
result = pack_bitstring(self.bits)
|
||||
packet = struct.pack(">B", len(result)) + result
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode response pdu.
|
||||
|
||||
:param data: The packet data to decode
|
||||
"""
|
||||
self.byte_count = int(data[0]) # pylint: disable=attribute-defined-outside-init
|
||||
self.bits = unpack_bitstring(data[1:])
|
||||
|
||||
def setBit(self, address, value=1):
|
||||
"""Set the specified bit.
|
||||
|
||||
:param address: The bit to set
|
||||
:param value: The value to set the bit to
|
||||
"""
|
||||
self.bits[address] = bool(value)
|
||||
|
||||
def resetBit(self, address):
|
||||
"""Set the specified bit to 0.
|
||||
|
||||
:param address: The bit to reset
|
||||
"""
|
||||
self.setBit(address, 0)
|
||||
|
||||
def getBit(self, address):
|
||||
"""Get the specified bit's value.
|
||||
|
||||
:param address: The bit to query
|
||||
:returns: The value of the requested bit
|
||||
"""
|
||||
return self.bits[address]
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of the instance.
|
||||
|
||||
:returns: A string representation of the instance
|
||||
"""
|
||||
return f"{self.__class__.__name__}({len(self.bits)})"
|
||||
|
||||
|
||||
class ReadCoilsRequest(ReadBitsRequestBase):
|
||||
"""This function code is used to read from 1 to 2000(0x7d0) contiguous status of coils in a remote device.
|
||||
|
||||
The Request PDU specifies the starting
|
||||
address, ie the address of the first coil specified, and the number of
|
||||
coils. In the PDU Coils are addressed starting at zero. Therefore coils
|
||||
numbered 1-16 are addressed as 0-15.
|
||||
"""
|
||||
|
||||
function_code = 1
|
||||
function_code_name = "read_coils"
|
||||
|
||||
def __init__(self, address=None, count=None, slave=0, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param address: The address to start reading from
|
||||
:param count: The number of bits to read
|
||||
:param slave: Modbus slave slave ID
|
||||
"""
|
||||
ReadBitsRequestBase.__init__(self, address, count, slave, **kwargs)
|
||||
|
||||
def execute(self, context):
|
||||
"""Run a read coils request against a datastore.
|
||||
|
||||
Before running the request, we make sure that the request is in
|
||||
the max valid range (0x001-0x7d0). Next we make sure that the
|
||||
request is valid against the current datastore.
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: An initialized :py:class:`~pymodbus.register_read_message.ReadCoilsResponse`, or an :py:class:`~pymodbus.pdu.ExceptionResponse` if an error occurred
|
||||
"""
|
||||
if not (1 <= self.count <= 0x7D0):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, self.count):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
values = context.getValues(self.function_code, self.address, self.count)
|
||||
if isinstance(values, ExceptionResponse):
|
||||
return values
|
||||
return ReadCoilsResponse(values)
|
||||
|
||||
|
||||
class ReadCoilsResponse(ReadBitsResponseBase):
|
||||
"""The coils in the response message are packed as one coil per bit of the data field.
|
||||
|
||||
Status is indicated as 1= ON and 0= OFF. The LSB of the
|
||||
first data byte contains the output addressed in the query. The other
|
||||
coils follow toward the high order end of this byte, and from low order
|
||||
to high order in subsequent bytes.
|
||||
|
||||
If the returned output quantity is not a multiple of eight, the
|
||||
remaining bits in the final data byte will be padded with zeros
|
||||
(toward the high order end of the byte). The Byte Count field specifies
|
||||
the quantity of complete bytes of data.
|
||||
|
||||
The requested coils can be found in boolean form in the .bits list.
|
||||
"""
|
||||
|
||||
function_code = 1
|
||||
|
||||
def __init__(self, values=None, slave=0, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param values: The request values to respond with
|
||||
:param slave: Modbus slave slave ID
|
||||
"""
|
||||
ReadBitsResponseBase.__init__(self, values, slave, **kwargs)
|
||||
|
||||
|
||||
class ReadDiscreteInputsRequest(ReadBitsRequestBase):
|
||||
"""This function code is used to read from 1 to 2000(0x7d0).
|
||||
|
||||
Contiguous status of discrete inputs in a remote device. The Request PDU specifies the
|
||||
starting address, ie the address of the first input specified, and the
|
||||
number of inputs. In the PDU Discrete Inputs are addressed starting at
|
||||
zero. Therefore Discrete inputs numbered 1-16 are addressed as 0-15.
|
||||
"""
|
||||
|
||||
function_code = 2
|
||||
function_code_name = "read_discrete_input"
|
||||
|
||||
def __init__(self, address=None, count=None, slave=0, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param address: The address to start reading from
|
||||
:param count: The number of bits to read
|
||||
:param slave: Modbus slave slave ID
|
||||
"""
|
||||
ReadBitsRequestBase.__init__(self, address, count, slave, **kwargs)
|
||||
|
||||
def execute(self, context):
|
||||
"""Run a read discrete input request against a datastore.
|
||||
|
||||
Before running the request, we make sure that the request is in
|
||||
the max valid range (0x001-0x7d0). Next we make sure that the
|
||||
request is valid against the current datastore.
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: An initialized :py:class:`~pymodbus.register_read_message.ReadDiscreteInputsResponse`, or an :py:class:`~pymodbus.pdu.ExceptionResponse` if an error occurred
|
||||
"""
|
||||
if not (1 <= self.count <= 0x7D0):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, self.count):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
values = context.getValues(self.function_code, self.address, self.count)
|
||||
if isinstance(values, ExceptionResponse):
|
||||
return values
|
||||
return ReadDiscreteInputsResponse(values)
|
||||
|
||||
|
||||
class ReadDiscreteInputsResponse(ReadBitsResponseBase):
|
||||
"""The discrete inputs in the response message are packed as one input per bit of the data field.
|
||||
|
||||
Status is indicated as 1= ON; 0= OFF. The LSB of
|
||||
the first data byte contains the input addressed in the query. The other
|
||||
inputs follow toward the high order end of this byte, and from low order
|
||||
to high order in subsequent bytes.
|
||||
|
||||
If the returned input quantity is not a multiple of eight, the
|
||||
remaining bits in the final data byte will be padded with zeros
|
||||
(toward the high order end of the byte). The Byte Count field specifies
|
||||
the quantity of complete bytes of data.
|
||||
|
||||
The requested coils can be found in boolean form in the .bits list.
|
||||
"""
|
||||
|
||||
function_code = 2
|
||||
|
||||
def __init__(self, values=None, slave=0, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param values: The request values to respond with
|
||||
:param slave: Modbus slave slave ID
|
||||
"""
|
||||
ReadBitsResponseBase.__init__(self, values, slave, **kwargs)
|
||||
292
env/lib/python3.11/site-packages/pymodbus/bit_write_message.py
vendored
Normal file
292
env/lib/python3.11/site-packages/pymodbus/bit_write_message.py
vendored
Normal file
@@ -0,0 +1,292 @@
|
||||
"""Bit Writing Request/Response.
|
||||
|
||||
TODO write mask request/response
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
"WriteSingleCoilRequest",
|
||||
"WriteSingleCoilResponse",
|
||||
"WriteMultipleCoilsRequest",
|
||||
"WriteMultipleCoilsResponse",
|
||||
]
|
||||
|
||||
# pylint: disable=missing-type-doc
|
||||
import struct
|
||||
|
||||
from pymodbus.constants import ModbusStatus
|
||||
from pymodbus.pdu import ExceptionResponse, ModbusRequest, ModbusResponse
|
||||
from pymodbus.pdu import ModbusExceptions as merror
|
||||
from pymodbus.utilities import pack_bitstring, unpack_bitstring
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Local Constants
|
||||
# ---------------------------------------------------------------------------#
|
||||
# These are defined in the spec to turn a coil on/off
|
||||
# ---------------------------------------------------------------------------#
|
||||
_turn_coil_on = struct.pack(">H", ModbusStatus.ON)
|
||||
_turn_coil_off = struct.pack(">H", ModbusStatus.OFF)
|
||||
|
||||
|
||||
class WriteSingleCoilRequest(ModbusRequest):
|
||||
"""This function code is used to write a single output to either ON or OFF in a remote device.
|
||||
|
||||
The requested ON/OFF state is specified by a constant in the request
|
||||
data field. A value of FF 00 hex requests the output to be ON. A value
|
||||
of 00 00 requests it to be OFF. All other values are illegal and will
|
||||
not affect the output.
|
||||
|
||||
The Request PDU specifies the address of the coil to be forced. Coils
|
||||
are addressed starting at zero. Therefore coil numbered 1 is addressed
|
||||
as 0. The requested ON/OFF state is specified by a constant in the Coil
|
||||
Value field. A value of 0XFF00 requests the coil to be ON. A value of
|
||||
0X0000 requests the coil to be off. All other values are illegal and
|
||||
will not affect the coil.
|
||||
"""
|
||||
|
||||
function_code = 5
|
||||
function_code_name = "write_coil"
|
||||
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address=None, value=None, slave=None, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param address: The variable address to write
|
||||
:param value: The value to write at address
|
||||
"""
|
||||
ModbusRequest.__init__(self, slave=slave, **kwargs)
|
||||
self.address = address
|
||||
self.value = bool(value)
|
||||
|
||||
def encode(self):
|
||||
"""Encode write coil request.
|
||||
|
||||
:returns: The byte encoded message
|
||||
"""
|
||||
result = struct.pack(">H", self.address)
|
||||
if self.value:
|
||||
result += _turn_coil_on
|
||||
else:
|
||||
result += _turn_coil_off
|
||||
return result
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode a write coil request.
|
||||
|
||||
:param data: The packet data to decode
|
||||
"""
|
||||
self.address, value = struct.unpack(">HH", data)
|
||||
self.value = value == ModbusStatus.ON
|
||||
|
||||
def execute(self, context):
|
||||
"""Run a write coil request against a datastore.
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: The populated response or exception message
|
||||
"""
|
||||
# if self.value not in [ModbusStatus.Off, ModbusStatus.On]:
|
||||
# return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, 1):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
|
||||
result = context.setValues(self.function_code, self.address, [self.value])
|
||||
if isinstance(result, ExceptionResponse):
|
||||
return result
|
||||
values = context.getValues(self.function_code, self.address, 1)
|
||||
if isinstance(values, ExceptionResponse):
|
||||
return values
|
||||
return WriteSingleCoilResponse(self.address, values[0])
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""Get response pdu size.
|
||||
|
||||
Func_code (1 byte) + Output Address (2 byte) + Output Value (2 Bytes)
|
||||
:return:
|
||||
"""
|
||||
return 1 + 2 + 2
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of the instance.
|
||||
|
||||
:return: A string representation of the instance
|
||||
"""
|
||||
return f"WriteCoilRequest({self.address}, {self.value}) => "
|
||||
|
||||
|
||||
class WriteSingleCoilResponse(ModbusResponse):
|
||||
"""The normal response is an echo of the request.
|
||||
|
||||
Returned after the coil state has been written.
|
||||
"""
|
||||
|
||||
function_code = 5
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address=None, value=None, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param address: The variable address written to
|
||||
:param value: The value written at address
|
||||
"""
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.value = value
|
||||
|
||||
def encode(self):
|
||||
"""Encode write coil response.
|
||||
|
||||
:return: The byte encoded message
|
||||
"""
|
||||
result = struct.pack(">H", self.address)
|
||||
if self.value:
|
||||
result += _turn_coil_on
|
||||
else:
|
||||
result += _turn_coil_off
|
||||
return result
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode a write coil response.
|
||||
|
||||
:param data: The packet data to decode
|
||||
"""
|
||||
self.address, value = struct.unpack(">HH", data)
|
||||
self.value = value == ModbusStatus.ON
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of the instance.
|
||||
|
||||
:returns: A string representation of the instance
|
||||
"""
|
||||
return f"WriteCoilResponse({self.address}) => {self.value}"
|
||||
|
||||
|
||||
class WriteMultipleCoilsRequest(ModbusRequest):
|
||||
"""This function code is used to forcea sequence of coils.
|
||||
|
||||
To either ON or OFF in a remote device. The Request PDU specifies the coil
|
||||
references to be forced. Coils are addressed starting at zero. Therefore
|
||||
coil numbered 1 is addressed as 0.
|
||||
|
||||
The requested ON/OFF states are specified by contents of the request
|
||||
data field. A logical "1" in a bit position of the field requests the
|
||||
corresponding output to be ON. A logical "0" requests it to be OFF."
|
||||
"""
|
||||
|
||||
function_code = 15
|
||||
function_code_name = "write_coils"
|
||||
_rtu_byte_count_pos = 6
|
||||
|
||||
def __init__(self, address=None, values=None, slave=None, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param address: The starting request address
|
||||
:param values: The values to write
|
||||
"""
|
||||
ModbusRequest.__init__(self, slave=slave, **kwargs)
|
||||
self.address = address
|
||||
if values is None:
|
||||
values = []
|
||||
elif not hasattr(values, "__iter__"):
|
||||
values = [values]
|
||||
self.values = values
|
||||
self.byte_count = (len(self.values) + 7) // 8
|
||||
|
||||
def encode(self):
|
||||
"""Encode write coils request.
|
||||
|
||||
:returns: The byte encoded message
|
||||
"""
|
||||
count = len(self.values)
|
||||
self.byte_count = (count + 7) // 8
|
||||
packet = struct.pack(">HHB", self.address, count, self.byte_count)
|
||||
packet += pack_bitstring(self.values)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode a write coils request.
|
||||
|
||||
:param data: The packet data to decode
|
||||
"""
|
||||
self.address, count, self.byte_count = struct.unpack(">HHB", data[0:5])
|
||||
values = unpack_bitstring(data[5:])
|
||||
self.values = values[:count]
|
||||
|
||||
def execute(self, context):
|
||||
"""Run a write coils request against a datastore.
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: The populated response or exception message
|
||||
"""
|
||||
count = len(self.values)
|
||||
if not 1 <= count <= 0x07B0:
|
||||
return self.doException(merror.IllegalValue)
|
||||
if self.byte_count != (count + 7) // 8:
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, count):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
|
||||
result = context.setValues(self.function_code, self.address, self.values)
|
||||
if isinstance(result, ExceptionResponse):
|
||||
return result
|
||||
return WriteMultipleCoilsResponse(self.address, count)
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of the instance.
|
||||
|
||||
:returns: A string representation of the instance
|
||||
"""
|
||||
params = (self.address, len(self.values))
|
||||
return (
|
||||
"WriteNCoilRequest (%d) => %d " # pylint: disable=consider-using-f-string
|
||||
% params
|
||||
)
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""Get response pdu size.
|
||||
|
||||
Func_code (1 byte) + Output Address (2 byte) + Quantity of Outputs (2 Bytes)
|
||||
:return:
|
||||
"""
|
||||
return 1 + 2 + 2
|
||||
|
||||
|
||||
class WriteMultipleCoilsResponse(ModbusResponse):
|
||||
"""The normal response returns the function code.
|
||||
|
||||
Starting address, and quantity of coils forced.
|
||||
"""
|
||||
|
||||
function_code = 15
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address=None, count=None, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param address: The starting variable address written to
|
||||
:param count: The number of values written
|
||||
"""
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.count = count
|
||||
|
||||
def encode(self):
|
||||
"""Encode write coils response.
|
||||
|
||||
:returns: The byte encoded message
|
||||
"""
|
||||
return struct.pack(">HH", self.address, self.count)
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode a write coils response.
|
||||
|
||||
:param data: The packet data to decode
|
||||
"""
|
||||
self.address, self.count = struct.unpack(">HH", data)
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of the instance.
|
||||
|
||||
:returns: A string representation of the instance
|
||||
"""
|
||||
return f"WriteNCoilResponse({self.address}, {self.count})"
|
||||
19
env/lib/python3.11/site-packages/pymodbus/client/__init__.py
vendored
Normal file
19
env/lib/python3.11/site-packages/pymodbus/client/__init__.py
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
"""Client"""
|
||||
|
||||
__all__ = [
|
||||
"AsyncModbusSerialClient",
|
||||
"AsyncModbusTcpClient",
|
||||
"AsyncModbusTlsClient",
|
||||
"AsyncModbusUdpClient",
|
||||
"ModbusBaseClient",
|
||||
"ModbusSerialClient",
|
||||
"ModbusTcpClient",
|
||||
"ModbusTlsClient",
|
||||
"ModbusUdpClient",
|
||||
]
|
||||
|
||||
from pymodbus.client.base import ModbusBaseClient
|
||||
from pymodbus.client.serial import AsyncModbusSerialClient, ModbusSerialClient
|
||||
from pymodbus.client.tcp import AsyncModbusTcpClient, ModbusTcpClient
|
||||
from pymodbus.client.tls import AsyncModbusTlsClient, ModbusTlsClient
|
||||
from pymodbus.client.udp import AsyncModbusUdpClient, ModbusUdpClient
|
||||
BIN
env/lib/python3.11/site-packages/pymodbus/client/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/client/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/client/__pycache__/base.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/client/__pycache__/base.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/client/__pycache__/mixin.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/client/__pycache__/mixin.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/client/__pycache__/serial.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/client/__pycache__/serial.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/client/__pycache__/tcp.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/client/__pycache__/tcp.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/client/__pycache__/tls.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/client/__pycache__/tls.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/client/__pycache__/udp.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/client/__pycache__/udp.cpython-311.pyc
vendored
Normal file
Binary file not shown.
324
env/lib/python3.11/site-packages/pymodbus/client/base.py
vendored
Normal file
324
env/lib/python3.11/site-packages/pymodbus/client/base.py
vendored
Normal file
@@ -0,0 +1,324 @@
|
||||
"""Base for all clients."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import socket
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable
|
||||
|
||||
from pymodbus.client.mixin import ModbusClientMixin
|
||||
from pymodbus.exceptions import ConnectionException, ModbusIOException
|
||||
from pymodbus.factory import ClientDecoder
|
||||
from pymodbus.framer import ModbusFramer
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.pdu import ModbusRequest, ModbusResponse
|
||||
from pymodbus.transaction import DictTransactionManager
|
||||
from pymodbus.transport import CommParams, ModbusProtocol
|
||||
from pymodbus.utilities import ModbusTransactionState
|
||||
|
||||
|
||||
class ModbusBaseClient(ModbusClientMixin, ModbusProtocol):
|
||||
"""**ModbusBaseClient**
|
||||
|
||||
**Parameters common to all clients**:
|
||||
|
||||
:param framer: (optional) Modbus Framer class.
|
||||
:param timeout: (optional) Timeout for a request, in seconds.
|
||||
:param retries: (optional) Max number of retries per request.
|
||||
:param retry_on_empty: (optional) Retry on empty response.
|
||||
:param close_comm_on_error: (optional) Close connection on error.
|
||||
:param strict: (optional) Strict timing, 1.5 character between requests.
|
||||
:param broadcast_enable: (optional) True to treat id 0 as broadcast address.
|
||||
:param reconnect_delay: (optional) Minimum delay in milliseconds before reconnecting.
|
||||
:param reconnect_delay_max: (optional) Maximum delay in milliseconds before reconnecting.
|
||||
:param on_reconnect_callback: (optional) Function that will be called just before a reconnection attempt.
|
||||
:param no_resend_on_retry: (optional) Do not resend request when retrying due to missing response.
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
|
||||
.. tip::
|
||||
Common parameters and all external methods for all clients are documented here,
|
||||
and not repeated with each client.
|
||||
|
||||
.. tip::
|
||||
**reconnect_delay** doubles automatically with each unsuccessful connect, from
|
||||
**reconnect_delay** to **reconnect_delay_max**.
|
||||
Set `reconnect_delay=0` to avoid automatic reconnection.
|
||||
|
||||
:mod:`ModbusBaseClient` is normally not referenced outside :mod:`pymodbus`.
|
||||
|
||||
**Application methods, common to all clients**:
|
||||
"""
|
||||
|
||||
@dataclass
|
||||
class _params:
|
||||
"""Parameter class."""
|
||||
|
||||
retries: int = None
|
||||
retry_on_empty: bool = None
|
||||
close_comm_on_error: bool = None
|
||||
strict: bool = None
|
||||
broadcast_enable: bool = None
|
||||
reconnect_delay: int = None
|
||||
|
||||
source_address: tuple[str, int] = None
|
||||
|
||||
server_hostname: str = None
|
||||
|
||||
def __init__( # pylint: disable=too-many-arguments
|
||||
self,
|
||||
framer: type[ModbusFramer] = None,
|
||||
timeout: float = 3,
|
||||
retries: int = 3,
|
||||
retry_on_empty: bool = False,
|
||||
close_comm_on_error: bool = False,
|
||||
strict: bool = True,
|
||||
broadcast_enable: bool = False,
|
||||
reconnect_delay: float = 0.1,
|
||||
reconnect_delay_max: float = 300,
|
||||
on_reconnect_callback: Callable[[], None] | None = None,
|
||||
no_resend_on_retry: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize a client instance."""
|
||||
ModbusClientMixin.__init__(self)
|
||||
self.use_sync = kwargs.get("use_sync", False)
|
||||
setup_params = CommParams(
|
||||
comm_type=kwargs.get("CommType"),
|
||||
comm_name="comm",
|
||||
source_address=kwargs.get("source_address", ("0.0.0.0", 0)),
|
||||
reconnect_delay=reconnect_delay,
|
||||
reconnect_delay_max=reconnect_delay_max,
|
||||
timeout_connect=timeout,
|
||||
host=kwargs.get("host", None),
|
||||
port=kwargs.get("port", 0),
|
||||
sslctx=kwargs.get("sslctx", None),
|
||||
baudrate=kwargs.get("baudrate", None),
|
||||
bytesize=kwargs.get("bytesize", None),
|
||||
parity=kwargs.get("parity", None),
|
||||
stopbits=kwargs.get("stopbits", None),
|
||||
handle_local_echo=kwargs.get("handle_local_echo", False),
|
||||
)
|
||||
if not self.use_sync:
|
||||
ModbusProtocol.__init__(
|
||||
self,
|
||||
setup_params,
|
||||
False,
|
||||
)
|
||||
else:
|
||||
self.comm_params = setup_params
|
||||
self.params = self._params()
|
||||
self.params.retries = int(retries)
|
||||
self.params.retry_on_empty = bool(retry_on_empty)
|
||||
self.params.close_comm_on_error = bool(close_comm_on_error)
|
||||
self.params.strict = bool(strict)
|
||||
self.params.broadcast_enable = bool(broadcast_enable)
|
||||
self.on_reconnect_callback = on_reconnect_callback
|
||||
self.retry_on_empty: int = 0
|
||||
self.no_resend_on_retry = no_resend_on_retry
|
||||
self.slaves: list[int] = []
|
||||
|
||||
# Common variables.
|
||||
self.framer = framer(ClientDecoder(), self)
|
||||
self.transaction = DictTransactionManager(
|
||||
self, retries=retries, retry_on_empty=retry_on_empty, **kwargs
|
||||
)
|
||||
self.reconnect_delay_current = self.params.reconnect_delay
|
||||
self.use_udp = False
|
||||
self.state = ModbusTransactionState.IDLE
|
||||
self.last_frame_end: float = 0
|
||||
self.silent_interval: float = 0
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Client external interface
|
||||
# ----------------------------------------------------------------------- #
|
||||
@property
|
||||
def connected(self):
|
||||
"""Connect internal."""
|
||||
return True
|
||||
|
||||
def register(self, custom_response_class: ModbusResponse) -> None:
|
||||
"""Register a custom response class with the decoder (call **sync**).
|
||||
|
||||
:param custom_response_class: (optional) Modbus response class.
|
||||
:raises MessageRegisterException: Check exception text.
|
||||
|
||||
Use register() to add non-standard responses (like e.g. a login prompt) and
|
||||
have them interpreted automatically.
|
||||
"""
|
||||
self.framer.decoder.register(custom_response_class)
|
||||
|
||||
def close(self, reconnect=False) -> None:
|
||||
"""Close connection."""
|
||||
if reconnect:
|
||||
self.connection_lost(asyncio.TimeoutError("Server not responding"))
|
||||
else:
|
||||
self.transport_close()
|
||||
|
||||
def idle_time(self) -> float:
|
||||
"""Time before initiating next transaction (call **sync**).
|
||||
|
||||
Applications can call message functions without checking idle_time(),
|
||||
this is done automatically.
|
||||
"""
|
||||
if self.last_frame_end is None or self.silent_interval is None:
|
||||
return 0
|
||||
return self.last_frame_end + self.silent_interval
|
||||
|
||||
def execute(self, request: ModbusRequest = None) -> ModbusResponse:
|
||||
"""Execute request and get response (call **sync/async**).
|
||||
|
||||
:param request: The request to process
|
||||
:returns: The result of the request execution
|
||||
:raises ConnectionException: Check exception text.
|
||||
"""
|
||||
if self.use_sync:
|
||||
if not self.connect():
|
||||
raise ConnectionException(f"Failed to connect[{self!s}]")
|
||||
return self.transaction.execute(request)
|
||||
if not self.transport:
|
||||
raise ConnectionException(f"Not connected[{self!s}]")
|
||||
return self.async_execute(request)
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Merged client methods
|
||||
# ----------------------------------------------------------------------- #
|
||||
async def async_execute(self, request=None):
|
||||
"""Execute requests asynchronously."""
|
||||
request.transaction_id = self.transaction.getNextTID()
|
||||
packet = self.framer.buildPacket(request)
|
||||
|
||||
count = 0
|
||||
while count <= self.params.retries:
|
||||
if not count or not self.no_resend_on_retry:
|
||||
self.transport_send(packet)
|
||||
if self.params.broadcast_enable and not request.slave_id:
|
||||
resp = b"Broadcast write sent - no response expected"
|
||||
break
|
||||
try:
|
||||
req = self._build_response(request.transaction_id)
|
||||
resp = await asyncio.wait_for(
|
||||
req, timeout=self.comm_params.timeout_connect
|
||||
)
|
||||
break
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
count += 1
|
||||
if count > self.params.retries:
|
||||
self.close(reconnect=True)
|
||||
raise ModbusIOException(
|
||||
f"ERROR: No response received after {self.params.retries} retries"
|
||||
)
|
||||
|
||||
return resp
|
||||
|
||||
def callback_data(self, data: bytes, addr: tuple = None) -> int:
|
||||
"""Handle received data
|
||||
|
||||
returns number of bytes consumed
|
||||
"""
|
||||
self.framer.processIncomingPacket(data, self._handle_response, slave=0)
|
||||
return len(data)
|
||||
|
||||
def callback_disconnected(self, _reason: Exception) -> None:
|
||||
"""Handle lost connection"""
|
||||
for tid in list(self.transaction):
|
||||
self.raise_future(
|
||||
self.transaction.getTransaction(tid),
|
||||
ConnectionException("Connection lost during request"),
|
||||
)
|
||||
|
||||
async def connect(self):
|
||||
"""Connect to the modbus remote host."""
|
||||
|
||||
def raise_future(self, my_future, exc):
|
||||
"""Set exception of a future if not done."""
|
||||
if not my_future.done():
|
||||
my_future.set_exception(exc)
|
||||
|
||||
def _handle_response(self, reply, **_kwargs):
|
||||
"""Handle the processed response and link to correct deferred."""
|
||||
if reply is not None:
|
||||
tid = reply.transaction_id
|
||||
if handler := self.transaction.getTransaction(tid):
|
||||
if not handler.done():
|
||||
handler.set_result(reply)
|
||||
else:
|
||||
Log.debug("Unrequested message: {}", reply, ":str")
|
||||
|
||||
def _build_response(self, tid):
|
||||
"""Return a deferred response for the current request."""
|
||||
my_future = asyncio.Future()
|
||||
if not self.transport:
|
||||
self.raise_future(my_future, ConnectionException("Client is not connected"))
|
||||
else:
|
||||
self.transaction.addTransaction(my_future, tid)
|
||||
return my_future
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Internal methods
|
||||
# ----------------------------------------------------------------------- #
|
||||
def send(self, request):
|
||||
"""Send request.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
if self.state != ModbusTransactionState.RETRYING:
|
||||
Log.debug('New Transaction state "SENDING"')
|
||||
self.state = ModbusTransactionState.SENDING
|
||||
return request
|
||||
|
||||
def recv(self, size):
|
||||
"""Receive data.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
return size
|
||||
|
||||
@classmethod
|
||||
def _get_address_family(cls, address):
|
||||
"""Get the correct address family."""
|
||||
try:
|
||||
_ = socket.inet_pton(socket.AF_INET6, address)
|
||||
except OSError: # not a valid ipv6 address
|
||||
return socket.AF_INET
|
||||
return socket.AF_INET6
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# The magic methods
|
||||
# ----------------------------------------------------------------------- #
|
||||
def __enter__(self):
|
||||
"""Implement the client with enter block.
|
||||
|
||||
:returns: The current instance of the client
|
||||
:raises ConnectionException:
|
||||
"""
|
||||
|
||||
if not self.connect():
|
||||
raise ConnectionException(f"Failed to connect[{self.__str__()}]")
|
||||
return self
|
||||
|
||||
async def __aenter__(self):
|
||||
"""Implement the client with enter block.
|
||||
|
||||
:returns: The current instance of the client
|
||||
:raises ConnectionException:
|
||||
"""
|
||||
if not await self.connect():
|
||||
raise ConnectionException(f"Failed to connect[{self.__str__()}]")
|
||||
return self
|
||||
|
||||
def __exit__(self, klass, value, traceback):
|
||||
"""Implement the client with exit block."""
|
||||
self.close()
|
||||
|
||||
async def __aexit__(self, klass, value, traceback):
|
||||
"""Implement the client with exit block."""
|
||||
self.close()
|
||||
|
||||
def __str__(self):
|
||||
"""Build a string representation of the connection.
|
||||
|
||||
:returns: The string representation
|
||||
"""
|
||||
return (
|
||||
f"{self.__class__.__name__} {self.comm_params.host}:{self.comm_params.port}"
|
||||
)
|
||||
582
env/lib/python3.11/site-packages/pymodbus/client/mixin.py
vendored
Normal file
582
env/lib/python3.11/site-packages/pymodbus/client/mixin.py
vendored
Normal file
@@ -0,0 +1,582 @@
|
||||
"""Modbus Client Common."""
|
||||
import struct
|
||||
from enum import Enum
|
||||
from typing import Any, List, Tuple, Union
|
||||
|
||||
import pymodbus.bit_read_message as pdu_bit_read
|
||||
import pymodbus.bit_write_message as pdu_bit_write
|
||||
import pymodbus.diag_message as pdu_diag
|
||||
import pymodbus.file_message as pdu_file_msg
|
||||
import pymodbus.mei_message as pdu_mei
|
||||
import pymodbus.other_message as pdu_other_msg
|
||||
import pymodbus.register_read_message as pdu_reg_read
|
||||
import pymodbus.register_write_message as pdu_req_write
|
||||
from pymodbus.constants import INTERNAL_ERROR
|
||||
from pymodbus.exceptions import ModbusException
|
||||
from pymodbus.pdu import ModbusRequest, ModbusResponse
|
||||
|
||||
|
||||
class ModbusClientMixin: # pylint: disable=too-many-public-methods
|
||||
"""**ModbusClientMixin**.
|
||||
|
||||
This is an interface class to facilitate the sending requests/receiving responses like read_coils.
|
||||
execute() allows to make a call with non-standard or user defined function codes (remember to add a PDU
|
||||
in the transport class to interpret the request/response).
|
||||
|
||||
Simple modbus message call::
|
||||
|
||||
response = client.read_coils(1, 10)
|
||||
# or
|
||||
response = await client.read_coils(1, 10)
|
||||
|
||||
Advanced modbus message call::
|
||||
|
||||
request = ReadCoilsRequest(1,10)
|
||||
response = client.execute(request)
|
||||
# or
|
||||
request = ReadCoilsRequest(1,10)
|
||||
response = await client.execute(request)
|
||||
|
||||
.. tip::
|
||||
All methods can be used directly (synchronous) or
|
||||
with await <method> (asynchronous) depending on the client used.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize."""
|
||||
|
||||
def execute(self, request: ModbusRequest) -> ModbusResponse:
|
||||
"""Execute request (code ???).
|
||||
|
||||
:param request: Request to send
|
||||
:raises ModbusException:
|
||||
|
||||
Call with custom function codes.
|
||||
|
||||
.. tip::
|
||||
Response is not interpreted.
|
||||
"""
|
||||
raise ModbusException(INTERNAL_ERROR)
|
||||
|
||||
def read_coils(
|
||||
self, address: int, count: int = 1, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Read coils (code 0x01).
|
||||
|
||||
:param address: Start address to read from
|
||||
:param count: (optional) Number of coils to read
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_bit_read.ReadCoilsRequest(address, count, slave, **kwargs)
|
||||
)
|
||||
|
||||
def read_discrete_inputs(
|
||||
self, address: int, count: int = 1, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Read discrete inputs (code 0x02).
|
||||
|
||||
:param address: Start address to read from
|
||||
:param count: (optional) Number of coils to read
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_bit_read.ReadDiscreteInputsRequest(address, count, slave, **kwargs)
|
||||
)
|
||||
|
||||
def read_holding_registers(
|
||||
self, address: int, count: int = 1, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Read holding registers (code 0x03).
|
||||
|
||||
:param address: Start address to read from
|
||||
:param count: (optional) Number of coils to read
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_reg_read.ReadHoldingRegistersRequest(address, count, slave, **kwargs)
|
||||
)
|
||||
|
||||
def read_input_registers(
|
||||
self, address: int, count: int = 1, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Read input registers (code 0x04).
|
||||
|
||||
:param address: Start address to read from
|
||||
:param count: (optional) Number of coils to read
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_reg_read.ReadInputRegistersRequest(address, count, slave, **kwargs)
|
||||
)
|
||||
|
||||
def write_coil(
|
||||
self, address: int, value: bool, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Write single coil (code 0x05).
|
||||
|
||||
:param address: Address to write to
|
||||
:param value: Boolean to write
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_bit_write.WriteSingleCoilRequest(address, value, slave, **kwargs)
|
||||
)
|
||||
|
||||
def write_register(
|
||||
self, address: int, value: int, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Write register (code 0x06).
|
||||
|
||||
:param address: Address to write to
|
||||
:param value: Value to write
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_req_write.WriteSingleRegisterRequest(address, value, slave, **kwargs)
|
||||
)
|
||||
|
||||
def read_exception_status(self, slave: int = 0, **kwargs: Any) -> ModbusResponse:
|
||||
"""Read Exception Status (code 0x07).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_other_msg.ReadExceptionStatusRequest(slave, **kwargs))
|
||||
|
||||
def diag_query_data(
|
||||
self, msg: bytearray, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose query data (code 0x08 sub 0x00).
|
||||
|
||||
:param msg: Message to be returned
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_diag.ReturnQueryDataRequest(msg, slave=slave, **kwargs))
|
||||
|
||||
def diag_restart_communication(
|
||||
self, toggle: bool, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose restart communication (code 0x08 sub 0x01).
|
||||
|
||||
:param toggle: True if toggled.
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_diag.RestartCommunicationsOptionRequest(toggle, slave=slave, **kwargs)
|
||||
)
|
||||
|
||||
def diag_read_diagnostic_register(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose read diagnostic register (code 0x08 sub 0x02).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_diag.ReturnDiagnosticRegisterRequest(slave=slave, **kwargs)
|
||||
)
|
||||
|
||||
def diag_change_ascii_input_delimeter(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose change ASCII input delimiter (code 0x08 sub 0x03).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_diag.ChangeAsciiInputDelimiterRequest(slave=slave, **kwargs)
|
||||
)
|
||||
|
||||
def diag_force_listen_only(self, slave: int = 0, **kwargs: Any) -> ModbusResponse:
|
||||
"""Diagnose force listen only (code 0x08 sub 0x04).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_diag.ForceListenOnlyModeRequest(slave=slave, **kwargs))
|
||||
|
||||
def diag_clear_counters(self, slave: int = 0, **kwargs: Any) -> ModbusResponse:
|
||||
"""Diagnose clear counters (code 0x08 sub 0x0A).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_diag.ClearCountersRequest(slave=slave, **kwargs))
|
||||
|
||||
def diag_read_bus_message_count(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose read bus message count (code 0x08 sub 0x0B).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_diag.ReturnBusMessageCountRequest(slave=slave, **kwargs)
|
||||
)
|
||||
|
||||
def diag_read_bus_comm_error_count(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose read Bus Communication Error Count (code 0x08 sub 0x0C).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_diag.ReturnBusCommunicationErrorCountRequest(slave=slave, **kwargs)
|
||||
)
|
||||
|
||||
def diag_read_bus_exception_error_count(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose read Bus Exception Error Count (code 0x08 sub 0x0D).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_diag.ReturnBusExceptionErrorCountRequest(slave=slave, **kwargs)
|
||||
)
|
||||
|
||||
def diag_read_slave_message_count(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose read Slave Message Count (code 0x08 sub 0x0E).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_diag.ReturnSlaveMessageCountRequest(slave=slave, **kwargs)
|
||||
)
|
||||
|
||||
def diag_read_slave_no_response_count(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose read Slave No Response Count (code 0x08 sub 0x0F).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_diag.ReturnSlaveNoResponseCountRequest(slave=slave, **kwargs)
|
||||
)
|
||||
|
||||
def diag_read_slave_nak_count(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose read Slave NAK Count (code 0x08 sub 0x10).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_diag.ReturnSlaveNAKCountRequest(slave=slave, **kwargs))
|
||||
|
||||
def diag_read_slave_busy_count(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose read Slave Busy Count (code 0x08 sub 0x11).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_diag.ReturnSlaveBusyCountRequest(slave=slave, **kwargs))
|
||||
|
||||
def diag_read_bus_char_overrun_count(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose read Bus Character Overrun Count (code 0x08 sub 0x12).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_diag.ReturnSlaveBusCharacterOverrunCountRequest(slave=slave, **kwargs)
|
||||
)
|
||||
|
||||
def diag_read_iop_overrun_count(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose read Iop overrun count (code 0x08 sub 0x13).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_diag.ReturnIopOverrunCountRequest(slave=slave, **kwargs)
|
||||
)
|
||||
|
||||
def diag_clear_overrun_counter(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose Clear Overrun Counter and Flag (code 0x08 sub 0x14).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_diag.ClearOverrunCountRequest(slave=slave, **kwargs))
|
||||
|
||||
def diag_getclear_modbus_response(
|
||||
self, slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Diagnose Get/Clear modbus plus (code 0x08 sub 0x15).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_diag.GetClearModbusPlusRequest(slave=slave, **kwargs))
|
||||
|
||||
def diag_get_comm_event_counter(self, **kwargs: Any) -> ModbusResponse:
|
||||
"""Diagnose get event counter (code 0x0B).
|
||||
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_other_msg.GetCommEventCounterRequest(**kwargs))
|
||||
|
||||
def diag_get_comm_event_log(self, **kwargs: Any) -> ModbusResponse:
|
||||
"""Diagnose get event counter (code 0x0C).
|
||||
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_other_msg.GetCommEventLogRequest(**kwargs))
|
||||
|
||||
def write_coils(
|
||||
self,
|
||||
address: int,
|
||||
values: Union[List[bool], bool],
|
||||
slave: int = 0,
|
||||
**kwargs: Any,
|
||||
) -> ModbusResponse:
|
||||
"""Write coils (code 0x0F).
|
||||
|
||||
:param address: Start address to write to
|
||||
:param values: List of booleans to write, or a single boolean to write
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_bit_write.WriteMultipleCoilsRequest(address, values, slave, **kwargs)
|
||||
)
|
||||
|
||||
def write_registers(
|
||||
self, address: int, values: Union[List[int], int], slave: int = 0, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Write registers (code 0x10).
|
||||
|
||||
:param address: Start address to write to
|
||||
:param values: List of values to write, or a single value to write
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_req_write.WriteMultipleRegistersRequest(
|
||||
address, values, slave, **kwargs
|
||||
)
|
||||
)
|
||||
|
||||
def report_slave_id(self, slave: int = 0, **kwargs: Any) -> ModbusResponse:
|
||||
"""Report slave ID (code 0x11).
|
||||
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_other_msg.ReportSlaveIdRequest(slave, **kwargs))
|
||||
|
||||
def read_file_record(self, records: List[Tuple], **kwargs: Any) -> ModbusResponse:
|
||||
"""Read file record (code 0x14).
|
||||
|
||||
:param records: List of (Reference type, File number, Record Number, Record Length)
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_file_msg.ReadFileRecordRequest(records, **kwargs))
|
||||
|
||||
def write_file_record(self, records: List[Tuple], **kwargs: Any) -> ModbusResponse:
|
||||
"""Write file record (code 0x15).
|
||||
|
||||
:param records: List of (Reference type, File number, Record Number, Record Length)
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_file_msg.WriteFileRecordRequest(records, **kwargs))
|
||||
|
||||
def mask_write_register(
|
||||
self,
|
||||
address: int = 0x0000,
|
||||
and_mask: int = 0xFFFF,
|
||||
or_mask: int = 0x0000,
|
||||
**kwargs: Any,
|
||||
) -> ModbusResponse:
|
||||
"""Mask write register (code 0x16).
|
||||
|
||||
:param address: The mask pointer address (0x0000 to 0xffff)
|
||||
:param and_mask: The and bitmask to apply to the register address
|
||||
:param or_mask: The or bitmask to apply to the register address
|
||||
:param kwargs: (optional) Experimental parameters.
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_req_write.MaskWriteRegisterRequest(address, and_mask, or_mask, **kwargs)
|
||||
)
|
||||
|
||||
def readwrite_registers(
|
||||
self,
|
||||
read_address: int = 0,
|
||||
read_count: int = 0,
|
||||
write_address: int = 0,
|
||||
values: Union[List[int], int] = 0,
|
||||
slave: int = 0,
|
||||
**kwargs,
|
||||
) -> ModbusResponse:
|
||||
"""Read/Write registers (code 0x17).
|
||||
|
||||
:param read_address: The address to start reading from
|
||||
:param read_count: The number of registers to read from address
|
||||
:param write_address: The address to start writing to
|
||||
:param values: List of values to write, or a single value to write
|
||||
:param slave: (optional) Modbus slave ID
|
||||
:param kwargs:
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_reg_read.ReadWriteMultipleRegistersRequest(
|
||||
read_address=read_address,
|
||||
read_count=read_count,
|
||||
write_address=write_address,
|
||||
write_registers=values,
|
||||
slave=slave,
|
||||
**kwargs,
|
||||
)
|
||||
)
|
||||
|
||||
def read_fifo_queue(self, address: int = 0x0000, **kwargs: Any) -> ModbusResponse:
|
||||
"""Read FIFO queue (code 0x18).
|
||||
|
||||
:param address: The address to start reading from
|
||||
:param kwargs:
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(pdu_file_msg.ReadFifoQueueRequest(address, **kwargs))
|
||||
|
||||
# code 0x2B sub 0x0D: CANopen General Reference Request and Response, NOT IMPLEMENTED
|
||||
|
||||
def read_device_information(
|
||||
self, read_code: int = None, object_id: int = 0x00, **kwargs: Any
|
||||
) -> ModbusResponse:
|
||||
"""Read FIFO queue (code 0x2B sub 0x0E).
|
||||
|
||||
:param read_code: The device information read code
|
||||
:param object_id: The object to read from
|
||||
:param kwargs:
|
||||
:raises ModbusException:
|
||||
"""
|
||||
return self.execute(
|
||||
pdu_mei.ReadDeviceInformationRequest(read_code, object_id, **kwargs)
|
||||
)
|
||||
|
||||
# ------------------
|
||||
# Converter methods
|
||||
# ------------------
|
||||
|
||||
class DATATYPE(Enum):
|
||||
"""Datatype enum (name and number of bytes), used for convert_* calls."""
|
||||
|
||||
INT16 = ("h", 1)
|
||||
UINT16 = ("H", 1)
|
||||
INT32 = ("i", 2)
|
||||
UINT32 = ("I", 2)
|
||||
INT64 = ("q", 4)
|
||||
UINT64 = ("Q", 4)
|
||||
FLOAT32 = ("f", 2)
|
||||
FLOAT64 = ("d", 4)
|
||||
STRING = ("s", 0)
|
||||
|
||||
@classmethod
|
||||
def convert_from_registers(
|
||||
cls, registers: List[int], data_type: DATATYPE
|
||||
) -> Union[int, float, str]:
|
||||
"""Convert registers to int/float/str.
|
||||
|
||||
:param registers: list of registers received from e.g. read_holding_registers()
|
||||
:param data_type: data type to convert to
|
||||
:returns: int, float or str depending on "data_type"
|
||||
:raises ModbusException: when size of registers is not 1, 2 or 4
|
||||
"""
|
||||
byte_list = bytearray()
|
||||
for x in registers:
|
||||
byte_list.extend(int.to_bytes(x, 2, "big"))
|
||||
if data_type == cls.DATATYPE.STRING:
|
||||
if byte_list[-1:] == b"\00":
|
||||
byte_list = byte_list[:-1]
|
||||
return byte_list.decode("utf-8")
|
||||
if len(registers) != data_type.value[1]:
|
||||
raise ModbusException(
|
||||
f"Illegal size ({len(registers)}) of register array, cannot convert!"
|
||||
)
|
||||
return struct.unpack(f">{data_type.value[0]}", byte_list)[0]
|
||||
|
||||
@classmethod
|
||||
def convert_to_registers(
|
||||
cls, value: Union[int, float, str], data_type: DATATYPE
|
||||
) -> List[int]:
|
||||
"""Convert int/float/str to registers (16/32/64 bit).
|
||||
|
||||
:param value: value to be converted
|
||||
:param data_type: data type to be encoded as registers
|
||||
:returns: List of registers, can be used directly in e.g. write_registers()
|
||||
:raises TypeError: when there is a mismatch between data_type and value
|
||||
"""
|
||||
if data_type == cls.DATATYPE.STRING:
|
||||
if not isinstance(value, str):
|
||||
raise TypeError(f"Value should be string but is {type(value)}.")
|
||||
byte_list = value.encode()
|
||||
if len(byte_list) % 2:
|
||||
byte_list += b"\x00"
|
||||
else:
|
||||
byte_list = struct.pack(f">{data_type.value[0]}", value)
|
||||
regs = [
|
||||
int.from_bytes(byte_list[x : x + 2], "big")
|
||||
for x in range(0, len(byte_list), 2)
|
||||
]
|
||||
return regs
|
||||
291
env/lib/python3.11/site-packages/pymodbus/client/serial.py
vendored
Normal file
291
env/lib/python3.11/site-packages/pymodbus/client/serial.py
vendored
Normal file
@@ -0,0 +1,291 @@
|
||||
"""Modbus client async serial communication."""
|
||||
import asyncio
|
||||
import time
|
||||
from contextlib import suppress
|
||||
from functools import partial
|
||||
from typing import Any, Type
|
||||
|
||||
from pymodbus.client.base import ModbusBaseClient
|
||||
from pymodbus.exceptions import ConnectionException
|
||||
from pymodbus.framer import ModbusFramer
|
||||
from pymodbus.framer.rtu_framer import ModbusRtuFramer
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.transport import CommType
|
||||
from pymodbus.utilities import ModbusTransactionState
|
||||
|
||||
|
||||
with suppress(ImportError):
|
||||
import serial
|
||||
|
||||
|
||||
class AsyncModbusSerialClient(ModbusBaseClient, asyncio.Protocol):
|
||||
"""**AsyncModbusSerialClient**.
|
||||
|
||||
:param port: Serial port used for communication.
|
||||
:param framer: (optional) Framer class.
|
||||
:param baudrate: (optional) Bits per second.
|
||||
:param bytesize: (optional) Number of bits per byte 7-8.
|
||||
:param parity: (optional) 'E'ven, 'O'dd or 'N'one
|
||||
:param stopbits: (optional) Number of stop bits 0-2¡.
|
||||
:param handle_local_echo: (optional) Discard local echo from dongle.
|
||||
:param kwargs: (optional) Experimental parameters
|
||||
|
||||
The serial communication is RS-485 based, and usually used with a usb RS485 dongle.
|
||||
|
||||
Example::
|
||||
|
||||
from pymodbus.client import AsyncModbusSerialClient
|
||||
|
||||
async def run():
|
||||
client = AsyncModbusSerialClient("dev/serial0")
|
||||
|
||||
await client.connect()
|
||||
...
|
||||
client.close()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
port: str,
|
||||
framer: Type[ModbusFramer] = ModbusRtuFramer,
|
||||
baudrate: int = 19200,
|
||||
bytesize: int = 8,
|
||||
parity: str = "N",
|
||||
stopbits: int = 1,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize Asyncio Modbus Serial Client."""
|
||||
asyncio.Protocol.__init__(self)
|
||||
ModbusBaseClient.__init__(
|
||||
self,
|
||||
framer=framer,
|
||||
CommType=CommType.SERIAL,
|
||||
host=port,
|
||||
baudrate=baudrate,
|
||||
bytesize=bytesize,
|
||||
parity=parity,
|
||||
stopbits=stopbits,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""Connect internal."""
|
||||
return self.is_active()
|
||||
|
||||
async def connect(self) -> bool:
|
||||
"""Connect Async client."""
|
||||
self.reset_delay()
|
||||
Log.debug("Connecting to {}.", self.comm_params.host)
|
||||
return await self.transport_connect()
|
||||
|
||||
|
||||
class ModbusSerialClient(ModbusBaseClient):
|
||||
"""**ModbusSerialClient**.
|
||||
|
||||
:param port: Serial port used for communication.
|
||||
:param framer: (optional) Framer class.
|
||||
:param baudrate: (optional) Bits per second.
|
||||
:param bytesize: (optional) Number of bits per byte 7-8.
|
||||
:param parity: (optional) 'E'ven, 'O'dd or 'N'one
|
||||
:param stopbits: (optional) Number of stop bits 0-2¡.
|
||||
:param handle_local_echo: (optional) Discard local echo from dongle.
|
||||
:param kwargs: (optional) Experimental parameters
|
||||
|
||||
The serial communication is RS-485 based, and usually used with a usb RS485 dongle.
|
||||
|
||||
Example::
|
||||
|
||||
from pymodbus.client import ModbusSerialClient
|
||||
|
||||
def run():
|
||||
client = ModbusSerialClient("dev/serial0")
|
||||
|
||||
client.connect()
|
||||
...
|
||||
client.close()
|
||||
|
||||
|
||||
Remark: There are no automatic reconnect as with AsyncModbusSerialClient
|
||||
"""
|
||||
|
||||
state = ModbusTransactionState.IDLE
|
||||
inter_char_timeout: float = 0
|
||||
silent_interval: float = 0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
port: str,
|
||||
framer: Type[ModbusFramer] = ModbusRtuFramer,
|
||||
baudrate: int = 19200,
|
||||
bytesize: int = 8,
|
||||
parity: str = "N",
|
||||
stopbits: int = 1,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize Modbus Serial Client."""
|
||||
self.transport = None
|
||||
kwargs["use_sync"] = True
|
||||
ModbusBaseClient.__init__(
|
||||
self,
|
||||
framer=framer,
|
||||
CommType=CommType.SERIAL,
|
||||
host=port,
|
||||
baudrate=baudrate,
|
||||
bytesize=bytesize,
|
||||
parity=parity,
|
||||
stopbits=stopbits,
|
||||
**kwargs,
|
||||
)
|
||||
self.socket = None
|
||||
|
||||
self.last_frame_end = None
|
||||
|
||||
self._t0 = float(1 + 8 + 2) / self.comm_params.baudrate
|
||||
|
||||
"""
|
||||
The minimum delay is 0.01s and the maximum can be set to 0.05s.
|
||||
Setting too large a setting affects efficiency.
|
||||
"""
|
||||
self._recv_interval = (
|
||||
(round((100 * self._t0), 2) + 0.01)
|
||||
if (round((100 * self._t0), 2) + 0.01) < 0.05
|
||||
else 0.05
|
||||
)
|
||||
|
||||
if self.comm_params.baudrate > 19200:
|
||||
self.silent_interval = 1.75 / 1000 # ms
|
||||
else:
|
||||
self.inter_char_timeout = 1.5 * self._t0
|
||||
self.silent_interval = 3.5 * self._t0
|
||||
self.silent_interval = round(self.silent_interval, 6)
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""Connect internal."""
|
||||
return self.connect()
|
||||
|
||||
def connect(self): # pylint: disable=invalid-overridden-method
|
||||
"""Connect to the modbus serial server."""
|
||||
if self.socket:
|
||||
return True
|
||||
try:
|
||||
self.socket = serial.serial_for_url(
|
||||
self.comm_params.host,
|
||||
timeout=self.comm_params.timeout_connect,
|
||||
bytesize=self.comm_params.bytesize,
|
||||
stopbits=self.comm_params.stopbits,
|
||||
baudrate=self.comm_params.baudrate,
|
||||
parity=self.comm_params.parity,
|
||||
)
|
||||
if isinstance(self.framer, ModbusRtuFramer):
|
||||
if self.params.strict:
|
||||
self.socket.interCharTimeout = self.inter_char_timeout
|
||||
self.last_frame_end = None
|
||||
except serial.SerialException as msg:
|
||||
Log.error("{}", msg)
|
||||
self.close()
|
||||
return self.socket is not None
|
||||
|
||||
def close(self): # pylint: disable=arguments-differ
|
||||
"""Close the underlying socket connection."""
|
||||
if self.socket:
|
||||
self.socket.close()
|
||||
self.socket = None
|
||||
|
||||
def _in_waiting(self):
|
||||
"""Return _in_waiting."""
|
||||
in_waiting = "in_waiting" if hasattr(self.socket, "in_waiting") else "inWaiting"
|
||||
|
||||
if in_waiting == "in_waiting":
|
||||
waitingbytes = getattr(self.socket, in_waiting)
|
||||
else:
|
||||
waitingbytes = getattr(self.socket, in_waiting)()
|
||||
return waitingbytes
|
||||
|
||||
def send(self, request):
|
||||
"""Send data on the underlying socket.
|
||||
|
||||
If receive buffer still holds some data then flush it.
|
||||
|
||||
Sleep if last send finished less than 3.5 character times ago.
|
||||
"""
|
||||
super().send(request)
|
||||
if not self.socket:
|
||||
raise ConnectionException(str(self))
|
||||
if request:
|
||||
try:
|
||||
if waitingbytes := self._in_waiting():
|
||||
result = self.socket.read(waitingbytes)
|
||||
if self.state == ModbusTransactionState.RETRYING:
|
||||
Log.debug(
|
||||
"Sending available data in recv buffer {}", result, ":hex"
|
||||
)
|
||||
return result
|
||||
Log.warning("Cleanup recv buffer before send: {}", result, ":hex")
|
||||
except NotImplementedError:
|
||||
pass
|
||||
if self.state != ModbusTransactionState.SENDING:
|
||||
Log.debug('New Transaction state "SENDING"')
|
||||
self.state = ModbusTransactionState.SENDING
|
||||
size = self.socket.write(request)
|
||||
return size
|
||||
return 0
|
||||
|
||||
def _wait_for_data(self):
|
||||
"""Wait for data."""
|
||||
size = 0
|
||||
more_data = False
|
||||
if (
|
||||
self.comm_params.timeout_connect is not None
|
||||
and self.comm_params.timeout_connect
|
||||
):
|
||||
condition = partial(
|
||||
lambda start, timeout: (time.time() - start) <= timeout,
|
||||
timeout=self.comm_params.timeout_connect,
|
||||
)
|
||||
else:
|
||||
condition = partial(lambda dummy1, dummy2: True, dummy2=None)
|
||||
start = time.time()
|
||||
while condition(start):
|
||||
available = self._in_waiting()
|
||||
if (more_data and not available) or (more_data and available == size):
|
||||
break
|
||||
if available and available != size:
|
||||
more_data = True
|
||||
size = available
|
||||
time.sleep(self._recv_interval)
|
||||
return size
|
||||
|
||||
def recv(self, size):
|
||||
"""Read data from the underlying descriptor."""
|
||||
super().recv(size)
|
||||
if not self.socket:
|
||||
raise ConnectionException(
|
||||
self.__str__() # pylint: disable=unnecessary-dunder-call
|
||||
)
|
||||
if size is None:
|
||||
size = self._wait_for_data()
|
||||
if size > self._in_waiting():
|
||||
self._wait_for_data()
|
||||
result = self.socket.read(size)
|
||||
return result
|
||||
|
||||
def is_socket_open(self):
|
||||
"""Check if socket is open."""
|
||||
if self.socket:
|
||||
if hasattr(self.socket, "is_open"):
|
||||
return self.socket.is_open
|
||||
return self.socket.isOpen()
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
"""Build a string representation of the connection."""
|
||||
return f"ModbusSerialClient({self.framer} baud[{self.comm_params.baudrate}])"
|
||||
|
||||
def __repr__(self):
|
||||
"""Return string representation."""
|
||||
return (
|
||||
f"<{self.__class__.__name__} at {hex(id(self))} socket={self.socket}, "
|
||||
f"framer={self.framer}, timeout={self.comm_params.timeout_connect}>"
|
||||
)
|
||||
275
env/lib/python3.11/site-packages/pymodbus/client/tcp.py
vendored
Normal file
275
env/lib/python3.11/site-packages/pymodbus/client/tcp.py
vendored
Normal file
@@ -0,0 +1,275 @@
|
||||
"""Modbus client async TCP communication."""
|
||||
import asyncio
|
||||
import select
|
||||
import socket
|
||||
import time
|
||||
from typing import Any, Tuple, Type
|
||||
|
||||
from pymodbus.client.base import ModbusBaseClient
|
||||
from pymodbus.exceptions import ConnectionException
|
||||
from pymodbus.framer import ModbusFramer
|
||||
from pymodbus.framer.socket_framer import ModbusSocketFramer
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.transport import CommType
|
||||
from pymodbus.utilities import ModbusTransactionState
|
||||
|
||||
|
||||
class AsyncModbusTcpClient(ModbusBaseClient, asyncio.Protocol):
|
||||
"""**AsyncModbusTcpClient**.
|
||||
|
||||
:param host: Host IP address or host name
|
||||
:param port: (optional) Port used for communication
|
||||
:param framer: (optional) Framer class
|
||||
:param source_address: (optional) source address of client
|
||||
:param kwargs: (optional) Experimental parameters
|
||||
|
||||
Example::
|
||||
|
||||
from pymodbus.client import AsyncModbusTcpClient
|
||||
|
||||
async def run():
|
||||
client = AsyncModbusTcpClient("localhost")
|
||||
|
||||
await client.connect()
|
||||
...
|
||||
client.close()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
port: int = 502,
|
||||
framer: Type[ModbusFramer] = ModbusSocketFramer,
|
||||
source_address: Tuple[str, int] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize Asyncio Modbus TCP Client."""
|
||||
asyncio.Protocol.__init__(self)
|
||||
if "CommType" not in kwargs:
|
||||
kwargs["CommType"] = CommType.TCP
|
||||
if source_address:
|
||||
kwargs["source_address"] = source_address
|
||||
ModbusBaseClient.__init__(
|
||||
self,
|
||||
framer=framer,
|
||||
host=host,
|
||||
port=port,
|
||||
**kwargs,
|
||||
)
|
||||
self.params.source_address = source_address
|
||||
|
||||
async def connect(self) -> bool:
|
||||
"""Initiate connection to start client."""
|
||||
self.reset_delay()
|
||||
Log.debug(
|
||||
"Connecting to {}:{}.",
|
||||
self.comm_params.host,
|
||||
self.comm_params.port,
|
||||
)
|
||||
return await self.transport_connect()
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""Return true if connected."""
|
||||
return self.is_active()
|
||||
|
||||
|
||||
class ModbusTcpClient(ModbusBaseClient):
|
||||
"""**ModbusTcpClient**.
|
||||
|
||||
:param host: Host IP address or host name
|
||||
:param port: (optional) Port used for communication
|
||||
:param framer: (optional) Framer class
|
||||
:param source_address: (optional) source address of client
|
||||
:param kwargs: (optional) Experimental parameters
|
||||
|
||||
Example::
|
||||
|
||||
from pymodbus.client import ModbusTcpClient
|
||||
|
||||
async def run():
|
||||
client = ModbusTcpClient("localhost")
|
||||
|
||||
client.connect()
|
||||
...
|
||||
client.close()
|
||||
|
||||
Remark: There are no automatic reconnect as with AsyncModbusTcpClient
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
port: int = 502,
|
||||
framer: Type[ModbusFramer] = ModbusSocketFramer,
|
||||
source_address: Tuple[str, int] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize Modbus TCP Client."""
|
||||
if "CommType" not in kwargs:
|
||||
kwargs["CommType"] = CommType.TCP
|
||||
kwargs["use_sync"] = True
|
||||
self.transport = None
|
||||
super().__init__(framer=framer, host=host, port=port, **kwargs)
|
||||
self.params.source_address = source_address
|
||||
self.socket = None
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""Connect internal."""
|
||||
return self.socket is not None
|
||||
|
||||
def connect(self): # pylint: disable=invalid-overridden-method
|
||||
"""Connect to the modbus tcp server."""
|
||||
if self.socket:
|
||||
return True
|
||||
try:
|
||||
self.socket = socket.create_connection(
|
||||
(self.comm_params.host, self.comm_params.port),
|
||||
timeout=self.comm_params.timeout_connect,
|
||||
source_address=self.params.source_address,
|
||||
)
|
||||
Log.debug(
|
||||
"Connection to Modbus server established. Socket {}",
|
||||
self.socket.getsockname(),
|
||||
)
|
||||
except OSError as msg:
|
||||
Log.error(
|
||||
"Connection to ({}, {}) failed: {}",
|
||||
self.comm_params.host,
|
||||
self.comm_params.port,
|
||||
msg,
|
||||
)
|
||||
self.close()
|
||||
return self.socket is not None
|
||||
|
||||
def close(self): # pylint: disable=arguments-differ
|
||||
"""Close the underlying socket connection."""
|
||||
if self.socket:
|
||||
self.socket.close()
|
||||
self.socket = None
|
||||
|
||||
def _check_read_buffer(self):
|
||||
"""Check read buffer."""
|
||||
time_ = time.time()
|
||||
end = time_ + self.comm_params.timeout_connect
|
||||
data = None
|
||||
ready = select.select([self.socket], [], [], end - time_)
|
||||
if ready[0]:
|
||||
data = self.socket.recv(1024)
|
||||
return data
|
||||
|
||||
def send(self, request):
|
||||
"""Send data on the underlying socket."""
|
||||
super().send(request)
|
||||
if not self.socket:
|
||||
raise ConnectionException(str(self))
|
||||
if self.state == ModbusTransactionState.RETRYING:
|
||||
if data := self._check_read_buffer():
|
||||
return data
|
||||
|
||||
if request:
|
||||
return self.socket.send(request)
|
||||
return 0
|
||||
|
||||
def recv(self, size):
|
||||
"""Read data from the underlying descriptor."""
|
||||
super().recv(size)
|
||||
if not self.socket:
|
||||
raise ConnectionException(str(self))
|
||||
|
||||
# socket.recv(size) waits until it gets some data from the host but
|
||||
# not necessarily the entire response that can be fragmented in
|
||||
# many packets.
|
||||
# To avoid split responses to be recognized as invalid
|
||||
# messages and to be discarded, loops socket.recv until full data
|
||||
# is received or timeout is expired.
|
||||
# If timeout expires returns the read data, also if its length is
|
||||
# less than the expected size.
|
||||
self.socket.setblocking(0)
|
||||
|
||||
timeout = self.comm_params.timeout_connect
|
||||
|
||||
# If size isn't specified read up to 4096 bytes at a time.
|
||||
if size is None:
|
||||
recv_size = 4096
|
||||
else:
|
||||
recv_size = size
|
||||
|
||||
data = []
|
||||
data_length = 0
|
||||
time_ = time.time()
|
||||
end = time_ + timeout
|
||||
while recv_size > 0:
|
||||
try:
|
||||
ready = select.select([self.socket], [], [], end - time_)
|
||||
except ValueError:
|
||||
return self._handle_abrupt_socket_close(size, data, time.time() - time_)
|
||||
if ready[0]:
|
||||
if (recv_data := self.socket.recv(recv_size)) == b"":
|
||||
return self._handle_abrupt_socket_close(
|
||||
size, data, time.time() - time_
|
||||
)
|
||||
data.append(recv_data)
|
||||
data_length += len(recv_data)
|
||||
time_ = time.time()
|
||||
|
||||
# If size isn't specified continue to read until timeout expires.
|
||||
if size:
|
||||
recv_size = size - data_length
|
||||
|
||||
# Timeout is reduced also if some data has been received in order
|
||||
# to avoid infinite loops when there isn't an expected response
|
||||
# size and the slave sends noisy data continuously.
|
||||
if time_ > end:
|
||||
break
|
||||
|
||||
return b"".join(data)
|
||||
|
||||
def _handle_abrupt_socket_close(self, size, data, duration):
|
||||
"""Handle unexpected socket close by remote end.
|
||||
|
||||
Intended to be invoked after determining that the remote end
|
||||
has unexpectedly closed the connection, to clean up and handle
|
||||
the situation appropriately.
|
||||
|
||||
:param size: The number of bytes that was attempted to read
|
||||
:param data: The actual data returned
|
||||
:param duration: Duration from the read was first attempted
|
||||
until it was determined that the remote closed the
|
||||
socket
|
||||
:return: The more than zero bytes read from the remote end
|
||||
:raises ConnectionException: If the remote end didn't send any
|
||||
data at all before closing the connection.
|
||||
"""
|
||||
self.close()
|
||||
size_txt = size if size else "unbounded read"
|
||||
readsize = f"read of {size_txt} bytes"
|
||||
msg = (
|
||||
f"{self}: Connection unexpectedly closed "
|
||||
f"{duration} seconds into {readsize}"
|
||||
)
|
||||
if data:
|
||||
result = b"".join(data)
|
||||
Log.warning(" after returning {} bytes: {} ", len(result), result)
|
||||
return result
|
||||
msg += " without response from slave before it closed connection"
|
||||
raise ConnectionException(msg)
|
||||
|
||||
def is_socket_open(self):
|
||||
"""Check if socket is open."""
|
||||
return self.socket is not None
|
||||
|
||||
def __str__(self):
|
||||
"""Build a string representation of the connection.
|
||||
|
||||
:returns: The string representation
|
||||
"""
|
||||
return f"ModbusTcpClient({self.comm_params.host}:{self.comm_params.port})"
|
||||
|
||||
def __repr__(self):
|
||||
"""Return string representation."""
|
||||
return (
|
||||
f"<{self.__class__.__name__} at {hex(id(self))} socket={self.socket}, "
|
||||
f"ipaddr={self.comm_params.host}, port={self.comm_params.port}, timeout={self.comm_params.timeout_connect}>"
|
||||
)
|
||||
171
env/lib/python3.11/site-packages/pymodbus/client/tls.py
vendored
Normal file
171
env/lib/python3.11/site-packages/pymodbus/client/tls.py
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
"""Modbus client async TLS communication."""
|
||||
import socket
|
||||
import ssl
|
||||
from typing import Any, Type
|
||||
|
||||
from pymodbus.client.tcp import AsyncModbusTcpClient, ModbusTcpClient
|
||||
from pymodbus.framer import ModbusFramer
|
||||
from pymodbus.framer.tls_framer import ModbusTlsFramer
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.transport import CommParams, CommType
|
||||
|
||||
|
||||
class AsyncModbusTlsClient(AsyncModbusTcpClient):
|
||||
"""**AsyncModbusTlsClient**.
|
||||
|
||||
:param host: Host IP address or host name
|
||||
:param port: (optional) Port used for communication
|
||||
:param framer: (optional) Framer class
|
||||
:param source_address: (optional) Source address of client
|
||||
:param sslctx: (optional) SSLContext to use for TLS
|
||||
:param certfile: (optional) Cert file path for TLS server request
|
||||
:param keyfile: (optional) Key file path for TLS server request
|
||||
:param password: (optional) Password for for decrypting private key file
|
||||
:param server_hostname: (optional) Bind certificate to host
|
||||
:param kwargs: (optional) Experimental parameters
|
||||
|
||||
..tip::
|
||||
See ModbusBaseClient for common parameters.
|
||||
|
||||
Example::
|
||||
|
||||
from pymodbus.client import AsyncModbusTlsClient
|
||||
|
||||
async def run():
|
||||
client = AsyncModbusTlsClient("localhost")
|
||||
|
||||
await client.connect()
|
||||
...
|
||||
client.close()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
port: int = 802,
|
||||
framer: Type[ModbusFramer] = ModbusTlsFramer,
|
||||
sslctx: ssl.SSLContext = None,
|
||||
certfile: str = None,
|
||||
keyfile: str = None,
|
||||
password: str = None,
|
||||
server_hostname: str = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Initialize Asyncio Modbus TLS Client."""
|
||||
AsyncModbusTcpClient.__init__(
|
||||
self,
|
||||
host,
|
||||
port=port,
|
||||
framer=framer,
|
||||
CommType=CommType.TLS,
|
||||
sslctx=CommParams.generate_ssl(
|
||||
False, certfile, keyfile, password, sslctx=sslctx
|
||||
),
|
||||
**kwargs,
|
||||
)
|
||||
self.params.server_hostname = server_hostname
|
||||
|
||||
async def connect(self) -> bool:
|
||||
"""Initiate connection to start client."""
|
||||
self.reset_delay()
|
||||
Log.debug(
|
||||
"Connecting to {}:{}.",
|
||||
self.comm_params.host,
|
||||
self.comm_params.port,
|
||||
)
|
||||
return await self.transport_connect()
|
||||
|
||||
|
||||
class ModbusTlsClient(ModbusTcpClient):
|
||||
"""**ModbusTlsClient**.
|
||||
|
||||
:param host: Host IP address or host name
|
||||
:param port: (optional) Port used for communication
|
||||
:param framer: (optional) Framer class
|
||||
:param source_address: (optional) Source address of client
|
||||
:param sslctx: (optional) SSLContext to use for TLS
|
||||
:param certfile: (optional) Cert file path for TLS server request
|
||||
:param keyfile: (optional) Key file path for TLS server request
|
||||
:param password: (optional) Password for decrypting private key file
|
||||
:param server_hostname: (optional) Bind certificate to host
|
||||
:param kwargs: (optional) Experimental parameters
|
||||
|
||||
..tip::
|
||||
See ModbusBaseClient for common parameters.
|
||||
|
||||
Example::
|
||||
|
||||
from pymodbus.client import ModbusTlsClient
|
||||
|
||||
async def run():
|
||||
client = ModbusTlsClient("localhost")
|
||||
|
||||
client.connect()
|
||||
...
|
||||
client.close()
|
||||
|
||||
|
||||
Remark: There are no automatic reconnect as with AsyncModbusTlsClient
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
port: int = 802,
|
||||
framer: Type[ModbusFramer] = ModbusTlsFramer,
|
||||
sslctx: ssl.SSLContext = None,
|
||||
certfile: str = None,
|
||||
keyfile: str = None,
|
||||
password: str = None,
|
||||
server_hostname: str = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Initialize Modbus TLS Client."""
|
||||
self.transport = None
|
||||
super().__init__(
|
||||
host, CommType=CommType.TLS, port=port, framer=framer, **kwargs
|
||||
)
|
||||
self.sslctx = CommParams.generate_ssl(
|
||||
False, certfile, keyfile, password, sslctx=sslctx
|
||||
)
|
||||
self.params.server_hostname = server_hostname
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""Connect internal."""
|
||||
return self.transport is not None
|
||||
|
||||
def connect(self):
|
||||
"""Connect to the modbus tls server."""
|
||||
if self.socket:
|
||||
return True
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
if self.params.source_address:
|
||||
sock.bind(self.params.source_address)
|
||||
self.socket = self.sslctx.wrap_socket(
|
||||
sock, server_side=False, server_hostname=self.comm_params.host
|
||||
)
|
||||
self.socket.settimeout(self.comm_params.timeout_connect)
|
||||
self.socket.connect((self.comm_params.host, self.comm_params.port))
|
||||
except OSError as msg:
|
||||
Log.error(
|
||||
"Connection to ({}, {}) failed: {}",
|
||||
self.comm_params.host,
|
||||
self.comm_params.port,
|
||||
msg,
|
||||
)
|
||||
self.close()
|
||||
return self.socket is not None
|
||||
|
||||
def __str__(self):
|
||||
"""Build a string representation of the connection."""
|
||||
return f"ModbusTlsClient({self.comm_params.host}:{self.comm_params.port})"
|
||||
|
||||
def __repr__(self):
|
||||
"""Return string representation."""
|
||||
return (
|
||||
f"<{self.__class__.__name__} at {hex(id(self))} socket={self.socket}, "
|
||||
f"ipaddr={self.comm_params.host}, port={self.comm_params.port}, sslctx={self.sslctx}, "
|
||||
f"timeout={self.comm_params.timeout_connect}>"
|
||||
)
|
||||
190
env/lib/python3.11/site-packages/pymodbus/client/udp.py
vendored
Normal file
190
env/lib/python3.11/site-packages/pymodbus/client/udp.py
vendored
Normal file
@@ -0,0 +1,190 @@
|
||||
"""Modbus client async UDP communication."""
|
||||
import asyncio
|
||||
import socket
|
||||
from typing import Any, Tuple, Type
|
||||
|
||||
from pymodbus.client.base import ModbusBaseClient
|
||||
from pymodbus.exceptions import ConnectionException
|
||||
from pymodbus.framer import ModbusFramer
|
||||
from pymodbus.framer.socket_framer import ModbusSocketFramer
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.transport import CommType
|
||||
|
||||
|
||||
DGRAM_TYPE = socket.SOCK_DGRAM
|
||||
|
||||
|
||||
class AsyncModbusUdpClient(
|
||||
ModbusBaseClient, asyncio.Protocol, asyncio.DatagramProtocol
|
||||
):
|
||||
"""**AsyncModbusUdpClient**.
|
||||
|
||||
:param host: Host IP address or host name
|
||||
:param port: (optional) Port used for communication.
|
||||
:param framer: (optional) Framer class.
|
||||
:param source_address: (optional) source address of client,
|
||||
:param kwargs: (optional) Experimental parameters
|
||||
|
||||
..tip::
|
||||
See ModbusBaseClient for common parameters.
|
||||
|
||||
Example::
|
||||
|
||||
from pymodbus.client import AsyncModbusUdpClient
|
||||
|
||||
async def run():
|
||||
client = AsyncModbusUdpClient("localhost")
|
||||
|
||||
await client.connect()
|
||||
...
|
||||
client.close()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
port: int = 502,
|
||||
framer: Type[ModbusFramer] = ModbusSocketFramer,
|
||||
source_address: Tuple[str, int] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize Asyncio Modbus UDP Client."""
|
||||
asyncio.DatagramProtocol.__init__(self)
|
||||
asyncio.Protocol.__init__(self)
|
||||
ModbusBaseClient.__init__(
|
||||
self, framer=framer, CommType=CommType.UDP, host=host, port=port, **kwargs
|
||||
)
|
||||
self.params.source_address = source_address
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""Return true if connected."""
|
||||
return self.is_active()
|
||||
|
||||
async def connect(self) -> bool:
|
||||
"""Start reconnecting asynchronous udp client.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
self.reset_delay()
|
||||
Log.debug(
|
||||
"Connecting to {}:{}.",
|
||||
self.comm_params.host,
|
||||
self.comm_params.port,
|
||||
)
|
||||
return await self.transport_connect()
|
||||
|
||||
|
||||
class ModbusUdpClient(ModbusBaseClient):
|
||||
"""**ModbusUdpClient**.
|
||||
|
||||
:param host: Host IP address or host name
|
||||
:param port: (optional) Port used for communication.
|
||||
:param framer: (optional) Framer class.
|
||||
:param source_address: (optional) source address of client,
|
||||
:param kwargs: (optional) Experimental parameters
|
||||
|
||||
..tip::
|
||||
See ModbusBaseClient for common parameters.
|
||||
|
||||
Example::
|
||||
|
||||
from pymodbus.client import ModbusUdpClient
|
||||
|
||||
async def run():
|
||||
client = ModbusUdpClient("localhost")
|
||||
|
||||
client.connect()
|
||||
...
|
||||
client.close()
|
||||
|
||||
Remark: There are no automatic reconnect as with AsyncModbusUdpClient
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
port: int = 502,
|
||||
framer: Type[ModbusFramer] = ModbusSocketFramer,
|
||||
source_address: Tuple[str, int] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize Modbus UDP Client."""
|
||||
kwargs["use_sync"] = True
|
||||
self.transport = None
|
||||
super().__init__(
|
||||
framer=framer, port=port, host=host, CommType=CommType.UDP, **kwargs
|
||||
)
|
||||
self.params.source_address = source_address
|
||||
|
||||
self.socket = None
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""Connect internal."""
|
||||
return self.socket is not None
|
||||
|
||||
def connect(self): # pylint: disable=invalid-overridden-method
|
||||
"""Connect to the modbus tcp server.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
if self.socket:
|
||||
return True
|
||||
try:
|
||||
family = ModbusUdpClient._get_address_family(self.comm_params.host)
|
||||
self.socket = socket.socket(family, socket.SOCK_DGRAM)
|
||||
self.socket.settimeout(self.comm_params.timeout_connect)
|
||||
except OSError as exc:
|
||||
Log.error("Unable to create udp socket {}", exc)
|
||||
self.close()
|
||||
return self.socket is not None
|
||||
|
||||
def close(self): # pylint: disable=arguments-differ
|
||||
"""Close the underlying socket connection.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
self.socket = None
|
||||
|
||||
def send(self, request):
|
||||
"""Send data on the underlying socket.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
super().send(request)
|
||||
if not self.socket:
|
||||
raise ConnectionException(str(self))
|
||||
if request:
|
||||
return self.socket.sendto(
|
||||
request, (self.comm_params.host, self.comm_params.port)
|
||||
)
|
||||
return 0
|
||||
|
||||
def recv(self, size):
|
||||
"""Read data from the underlying descriptor.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
super().recv(size)
|
||||
if not self.socket:
|
||||
raise ConnectionException(str(self))
|
||||
return self.socket.recvfrom(size)[0]
|
||||
|
||||
def is_socket_open(self):
|
||||
"""Check if socket is open.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
"""Build a string representation of the connection."""
|
||||
return f"ModbusUdpClient({self.comm_params.host}:{self.comm_params.port})"
|
||||
|
||||
def __repr__(self):
|
||||
"""Return string representation."""
|
||||
return (
|
||||
f"<{self.__class__.__name__} at {hex(id(self))} socket={self.socket}, "
|
||||
f"ipaddr={self.comm_params.host}, port={self.comm_params.port}, timeout={self.comm_params.timeout_connect}>"
|
||||
)
|
||||
159
env/lib/python3.11/site-packages/pymodbus/constants.py
vendored
Normal file
159
env/lib/python3.11/site-packages/pymodbus/constants.py
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
"""Constants For Modbus Server/Client.
|
||||
|
||||
This is the single location for storing default
|
||||
values for the servers and clients.
|
||||
"""
|
||||
import enum
|
||||
|
||||
|
||||
INTERNAL_ERROR = "Pymodbus internal error"
|
||||
|
||||
|
||||
class ModbusStatus(int, enum.Enum):
|
||||
"""These represent various status codes in the modbus protocol.
|
||||
|
||||
.. attribute:: WAITING
|
||||
|
||||
This indicates that a modbus device is currently
|
||||
waiting for a given request to finish some running task.
|
||||
|
||||
.. attribute:: READY
|
||||
|
||||
This indicates that a modbus device is currently
|
||||
free to perform the next request task.
|
||||
|
||||
.. attribute:: ON
|
||||
|
||||
This indicates that the given modbus entity is on
|
||||
|
||||
.. attribute:: OFF
|
||||
|
||||
This indicates that the given modbus entity is off
|
||||
|
||||
.. attribute:: SLAVE_ON
|
||||
|
||||
This indicates that the given modbus slave is running
|
||||
|
||||
.. attribute:: SLAVE_OFF
|
||||
|
||||
This indicates that the given modbus slave is not running
|
||||
"""
|
||||
|
||||
WAITING = 0xFFFF
|
||||
READY = 0x0000
|
||||
ON = 0xFF00
|
||||
OFF = 0x0000
|
||||
SLAVE_ON = 0xFF
|
||||
SLAVE_OFF = 0x00
|
||||
|
||||
def __str__(self):
|
||||
"""Override to force int representation for enum members"""
|
||||
return str(int(self))
|
||||
|
||||
|
||||
class Endian(str, enum.Enum):
|
||||
"""An enumeration representing the various byte endianness.
|
||||
|
||||
.. attribute:: AUTO
|
||||
|
||||
This indicates that the byte order is chosen by the
|
||||
current native environment.
|
||||
|
||||
.. attribute:: BIG
|
||||
|
||||
This indicates that the bytes are in big endian format
|
||||
|
||||
.. attribute:: LITTLE
|
||||
|
||||
This indicates that the bytes are in little endian format
|
||||
|
||||
.. note:: I am simply borrowing the format strings from the
|
||||
python struct module for my convenience.
|
||||
"""
|
||||
|
||||
AUTO = "@"
|
||||
BIG = ">"
|
||||
LITTLE = "<"
|
||||
|
||||
def __str__(self):
|
||||
"""Override to force str representation for enum members"""
|
||||
return str.__str__(self)
|
||||
|
||||
|
||||
class ModbusPlusOperation(int, enum.Enum):
|
||||
"""Represents the type of modbus plus request.
|
||||
|
||||
.. attribute:: GET_STATISTICS
|
||||
|
||||
Operation requesting that the current modbus plus statistics
|
||||
be returned in the response.
|
||||
|
||||
.. attribute:: CLEAR_STATISTICS
|
||||
|
||||
Operation requesting that the current modbus plus statistics
|
||||
be cleared and not returned in the response.
|
||||
"""
|
||||
|
||||
GET_STATISTICS = 0x0003
|
||||
CLEAR_STATISTICS = 0x0004
|
||||
|
||||
def __str__(self):
|
||||
"""Override to force int representation for enum members"""
|
||||
return str(int(self))
|
||||
|
||||
|
||||
class DeviceInformation(int, enum.Enum):
|
||||
"""Represents what type of device information to read.
|
||||
|
||||
.. attribute:: BASIC
|
||||
|
||||
This is the basic (required) device information to be returned.
|
||||
This includes VendorName, ProductCode, and MajorMinorRevision
|
||||
code.
|
||||
|
||||
.. attribute:: REGULAR
|
||||
|
||||
In addition to basic data objects, the device provides additional
|
||||
and optional identification and description data objects. All of
|
||||
the objects of this category are defined in the standard but their
|
||||
implementation is optional.
|
||||
|
||||
.. attribute:: EXTENDED
|
||||
|
||||
In addition to regular data objects, the device provides additional
|
||||
and optional identification and description private data about the
|
||||
physical device itself. All of these data are device dependent.
|
||||
|
||||
.. attribute:: SPECIFIC
|
||||
|
||||
Request to return a single data object.
|
||||
"""
|
||||
|
||||
BASIC = 0x01
|
||||
REGULAR = 0x02
|
||||
EXTENDED = 0x03
|
||||
SPECIFIC = 0x04
|
||||
|
||||
def __str__(self):
|
||||
"""Override to force int representation for enum members"""
|
||||
return str(int(self))
|
||||
|
||||
|
||||
class MoreData(int, enum.Enum):
|
||||
"""Represents the more follows condition.
|
||||
|
||||
.. attribute:: NOTHING
|
||||
|
||||
This indicates that no more objects are going to be returned.
|
||||
|
||||
.. attribute:: KEEP_READING
|
||||
|
||||
This indicates that there are more objects to be returned.
|
||||
"""
|
||||
|
||||
NOTHING = 0x00
|
||||
KEEP_READING = 0xFF
|
||||
|
||||
def __str__(self):
|
||||
"""Override to force int representation for enum members"""
|
||||
return str(int(self))
|
||||
21
env/lib/python3.11/site-packages/pymodbus/datastore/__init__.py
vendored
Normal file
21
env/lib/python3.11/site-packages/pymodbus/datastore/__init__.py
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
"""Datastore."""
|
||||
|
||||
__all__ = [
|
||||
"ModbusBaseSlaveContext",
|
||||
"ModbusSequentialDataBlock",
|
||||
"ModbusSparseDataBlock",
|
||||
"ModbusSlaveContext",
|
||||
"ModbusServerContext",
|
||||
"ModbusSimulatorContext",
|
||||
]
|
||||
|
||||
from pymodbus.datastore.context import (
|
||||
ModbusBaseSlaveContext,
|
||||
ModbusServerContext,
|
||||
ModbusSlaveContext,
|
||||
)
|
||||
from pymodbus.datastore.simulator import ModbusSimulatorContext
|
||||
from pymodbus.datastore.store import (
|
||||
ModbusSequentialDataBlock,
|
||||
ModbusSparseDataBlock,
|
||||
)
|
||||
BIN
env/lib/python3.11/site-packages/pymodbus/datastore/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/datastore/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/datastore/__pycache__/context.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/datastore/__pycache__/context.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/datastore/__pycache__/remote.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/datastore/__pycache__/remote.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/datastore/__pycache__/simulator.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/datastore/__pycache__/simulator.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/datastore/__pycache__/store.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/datastore/__pycache__/store.cpython-311.pyc
vendored
Normal file
Binary file not shown.
195
env/lib/python3.11/site-packages/pymodbus/datastore/context.py
vendored
Normal file
195
env/lib/python3.11/site-packages/pymodbus/datastore/context.py
vendored
Normal file
@@ -0,0 +1,195 @@
|
||||
"""Context for datastore."""
|
||||
# pylint: disable=missing-type-doc
|
||||
from pymodbus.datastore.store import ModbusSequentialDataBlock
|
||||
from pymodbus.exceptions import NoSuchSlaveException
|
||||
from pymodbus.logging import Log
|
||||
|
||||
|
||||
class ModbusBaseSlaveContext: # pylint: disable=too-few-public-methods
|
||||
"""Interface for a modbus slave data context.
|
||||
|
||||
Derived classes must implemented the following methods:
|
||||
reset(self)
|
||||
validate(self, fx, address, count=1)
|
||||
getValues(self, fx, address, count=1)
|
||||
setValues(self, fx, address, values)
|
||||
"""
|
||||
|
||||
_fx_mapper = {2: "d", 4: "i"}
|
||||
_fx_mapper.update([(i, "h") for i in (3, 6, 16, 22, 23)])
|
||||
_fx_mapper.update([(i, "c") for i in (1, 5, 15)])
|
||||
|
||||
def decode(self, fx):
|
||||
"""Convert the function code to the datastore to.
|
||||
|
||||
:param fx: The function we are working with
|
||||
:returns: one of [d(iscretes),i(nputs),h(olding),c(oils)
|
||||
"""
|
||||
return self._fx_mapper[fx]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Slave Contexts
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ModbusSlaveContext(ModbusBaseSlaveContext):
|
||||
"""This creates a modbus data model with each data access stored in a block."""
|
||||
|
||||
def __init__(self, *_args, **kwargs):
|
||||
"""Initialize the datastores.
|
||||
|
||||
:param kwargs: Each element is a ModbusDataBlock
|
||||
|
||||
"di" - Discrete Inputs initializer
|
||||
"co" - Coils initializer
|
||||
"hr" - Holding Register initializer
|
||||
"ir" - Input Registers iniatializer
|
||||
"""
|
||||
self.store = {}
|
||||
self.store["d"] = kwargs.get("di", ModbusSequentialDataBlock.create())
|
||||
self.store["c"] = kwargs.get("co", ModbusSequentialDataBlock.create())
|
||||
self.store["i"] = kwargs.get("ir", ModbusSequentialDataBlock.create())
|
||||
self.store["h"] = kwargs.get("hr", ModbusSequentialDataBlock.create())
|
||||
self.zero_mode = kwargs.get("zero_mode", False)
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of the context.
|
||||
|
||||
:returns: A string representation of the context
|
||||
"""
|
||||
return "Modbus Slave Context"
|
||||
|
||||
def reset(self):
|
||||
"""Reset all the datastores to their default values."""
|
||||
for datastore in iter(self.store.values()):
|
||||
datastore.reset()
|
||||
|
||||
def validate(self, fc_as_hex, address, count=1):
|
||||
"""Validate the request to make sure it is in range.
|
||||
|
||||
:param fc_as_hex: The function we are working with
|
||||
:param address: The starting address
|
||||
:param count: The number of values to test
|
||||
:returns: True if the request in within range, False otherwise
|
||||
"""
|
||||
if not self.zero_mode:
|
||||
address += 1
|
||||
Log.debug("validate: fc-[{}] address-{}: count-{}", fc_as_hex, address, count)
|
||||
return self.store[self.decode(fc_as_hex)].validate(address, count)
|
||||
|
||||
def getValues(self, fc_as_hex, address, count=1):
|
||||
"""Get `count` values from datastore.
|
||||
|
||||
:param fc_as_hex: The function we are working with
|
||||
:param address: The starting address
|
||||
:param count: The number of values to retrieve
|
||||
:returns: The requested values from a:a+c
|
||||
"""
|
||||
if not self.zero_mode:
|
||||
address += 1
|
||||
Log.debug("getValues: fc-[{}] address-{}: count-{}", fc_as_hex, address, count)
|
||||
return self.store[self.decode(fc_as_hex)].getValues(address, count)
|
||||
|
||||
def setValues(self, fc_as_hex, address, values):
|
||||
"""Set the datastore with the supplied values.
|
||||
|
||||
:param fc_as_hex: The function we are working with
|
||||
:param address: The starting address
|
||||
:param values: The new values to be set
|
||||
"""
|
||||
if not self.zero_mode:
|
||||
address += 1
|
||||
Log.debug("setValues[{}] address-{}: count-{}", fc_as_hex, address, len(values))
|
||||
self.store[self.decode(fc_as_hex)].setValues(address, values)
|
||||
|
||||
def register(self, function_code, fc_as_hex, datablock=None):
|
||||
"""Register a datablock with the slave context.
|
||||
|
||||
:param function_code: function code (int)
|
||||
:param fc_as_hex: string representation of function code (e.g "cf" )
|
||||
:param datablock: datablock to associate with this function code
|
||||
"""
|
||||
self.store[fc_as_hex] = datablock or ModbusSequentialDataBlock.create()
|
||||
self._fx_mapper[function_code] = fc_as_hex
|
||||
|
||||
|
||||
class ModbusServerContext:
|
||||
"""This represents a master collection of slave contexts.
|
||||
|
||||
If single is set to true, it will be treated as a single
|
||||
context so every slave_id returns the same context. If single
|
||||
is set to false, it will be interpreted as a collection of
|
||||
slave contexts.
|
||||
"""
|
||||
|
||||
def __init__(self, slaves=None, single=True):
|
||||
"""Initialize a new instance of a modbus server context.
|
||||
|
||||
:param slaves: A dictionary of client contexts
|
||||
:param single: Set to true to treat this as a single context
|
||||
"""
|
||||
self.single = single
|
||||
self._slaves = slaves or {}
|
||||
if self.single:
|
||||
self._slaves = {0: self._slaves}
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over the current collection of slave contexts.
|
||||
|
||||
:returns: An iterator over the slave contexts
|
||||
"""
|
||||
return iter(self._slaves.items())
|
||||
|
||||
def __contains__(self, slave):
|
||||
"""Check if the given slave is in this list.
|
||||
|
||||
:param slave: slave The slave to check for existence
|
||||
:returns: True if the slave exists, False otherwise
|
||||
"""
|
||||
if self.single and self._slaves:
|
||||
return True
|
||||
return slave in self._slaves
|
||||
|
||||
def __setitem__(self, slave, context):
|
||||
"""Use to set a new slave context.
|
||||
|
||||
:param slave: The slave context to set
|
||||
:param context: The new context to set for this slave
|
||||
:raises NoSuchSlaveException:
|
||||
"""
|
||||
if self.single:
|
||||
slave = 0
|
||||
if 0xF7 >= slave >= 0x00:
|
||||
self._slaves[slave] = context
|
||||
else:
|
||||
raise NoSuchSlaveException(f"slave index :{slave} out of range")
|
||||
|
||||
def __delitem__(self, slave):
|
||||
"""Use to access the slave context.
|
||||
|
||||
:param slave: The slave context to remove
|
||||
:raises NoSuchSlaveException:
|
||||
"""
|
||||
if not self.single and (0xF7 >= slave >= 0x00):
|
||||
del self._slaves[slave]
|
||||
else:
|
||||
raise NoSuchSlaveException(f"slave index: {slave} out of range")
|
||||
|
||||
def __getitem__(self, slave):
|
||||
"""Use to get access to a slave context.
|
||||
|
||||
:param slave: The slave context to get
|
||||
:returns: The requested slave context
|
||||
:raises NoSuchSlaveException:
|
||||
"""
|
||||
if self.single:
|
||||
slave = 0
|
||||
if slave in self._slaves:
|
||||
return self._slaves.get(slave)
|
||||
raise NoSuchSlaveException(
|
||||
f"slave - {slave} does not exist, or is out of range"
|
||||
)
|
||||
|
||||
def slaves(self):
|
||||
"""Define slaves."""
|
||||
# Python3 now returns keys() as iterable
|
||||
return list(self._slaves.keys())
|
||||
129
env/lib/python3.11/site-packages/pymodbus/datastore/remote.py
vendored
Normal file
129
env/lib/python3.11/site-packages/pymodbus/datastore/remote.py
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
"""Remote datastore."""
|
||||
from pymodbus.datastore import ModbusBaseSlaveContext
|
||||
from pymodbus.exceptions import NotImplementedException
|
||||
from pymodbus.logging import Log
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Context
|
||||
# ---------------------------------------------------------------------------#
|
||||
class RemoteSlaveContext(ModbusBaseSlaveContext):
|
||||
"""TODO.
|
||||
|
||||
This creates a modbus data model that connects to
|
||||
a remote device (depending on the client used)
|
||||
"""
|
||||
|
||||
def __init__(self, client, slave=None):
|
||||
"""Initialize the datastores.
|
||||
|
||||
:param client: The client to retrieve values with
|
||||
:param slave: Unit ID of the remote slave
|
||||
"""
|
||||
self._client = client
|
||||
self.slave = slave
|
||||
self.result = None
|
||||
self.__build_mapping()
|
||||
if not self.__set_callbacks:
|
||||
Log.error("Init went wrong.")
|
||||
|
||||
def reset(self):
|
||||
"""Reset all the datastores to their default values."""
|
||||
raise NotImplementedException()
|
||||
|
||||
def validate(self, _fc_as_hex, _address, _count):
|
||||
"""Validate the request to make sure it is in range.
|
||||
|
||||
:returns: True
|
||||
"""
|
||||
return True
|
||||
|
||||
def getValues(self, fc_as_hex, _address, _count=1):
|
||||
"""Get values from real call in validate"""
|
||||
if fc_as_hex in self._write_fc:
|
||||
return [0]
|
||||
group_fx = self.decode(fc_as_hex)
|
||||
func_fc = self.__get_callbacks[group_fx]
|
||||
self.result = func_fc(_address, _count)
|
||||
return self.__extract_result(self.decode(fc_as_hex), self.result)
|
||||
|
||||
def setValues(self, fc_as_hex, address, values):
|
||||
"""Set the datastore with the supplied values."""
|
||||
group_fx = self.decode(fc_as_hex)
|
||||
if fc_as_hex in self._write_fc:
|
||||
func_fc = self.__set_callbacks[f"{group_fx}{fc_as_hex}"]
|
||||
if fc_as_hex in {0x0F, 0x10}:
|
||||
self.result = func_fc(address, values)
|
||||
else:
|
||||
self.result = func_fc(address, values[0])
|
||||
if self.result.isError():
|
||||
return self.result
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of the context.
|
||||
|
||||
:returns: A string representation of the context
|
||||
"""
|
||||
return f"Remote Slave Context({self._client})"
|
||||
|
||||
def __build_mapping(self):
|
||||
"""Build the function code mapper."""
|
||||
kwargs = {}
|
||||
if self.slave:
|
||||
kwargs["slave"] = self.slave
|
||||
self.__get_callbacks = {
|
||||
"d": lambda a, c: self._client.read_discrete_inputs( # pylint: disable=unnecessary-lambda
|
||||
a, c, **kwargs
|
||||
),
|
||||
"c": lambda a, c: self._client.read_coils( # pylint: disable=unnecessary-lambda
|
||||
a, c, **kwargs
|
||||
),
|
||||
"h": lambda a, c: self._client.read_holding_registers( # pylint: disable=unnecessary-lambda
|
||||
a, c, **kwargs
|
||||
),
|
||||
"i": lambda a, c: self._client.read_input_registers( # pylint: disable=unnecessary-lambda
|
||||
a, c, **kwargs
|
||||
),
|
||||
}
|
||||
self.__set_callbacks = {
|
||||
"d5": lambda a, v: self._client.write_coil( # pylint: disable=unnecessary-lambda
|
||||
a, v, **kwargs
|
||||
),
|
||||
"d15": lambda a, v: self._client.write_coils( # pylint: disable=unnecessary-lambda
|
||||
a, v, **kwargs
|
||||
),
|
||||
"c5": lambda a, v: self._client.write_coil( # pylint: disable=unnecessary-lambda
|
||||
a, v, **kwargs
|
||||
),
|
||||
"c15": lambda a, v: self._client.write_coils( # pylint: disable=unnecessary-lambda
|
||||
a, v, **kwargs
|
||||
),
|
||||
"h6": lambda a, v: self._client.write_register( # pylint: disable=unnecessary-lambda
|
||||
a, v, **kwargs
|
||||
),
|
||||
"h16": lambda a, v: self._client.write_registers( # pylint: disable=unnecessary-lambda
|
||||
a, v, **kwargs
|
||||
),
|
||||
"i6": lambda a, v: self._client.write_register( # pylint: disable=unnecessary-lambda
|
||||
a, v, **kwargs
|
||||
),
|
||||
"i16": lambda a, v: self._client.write_registers( # pylint: disable=unnecessary-lambda
|
||||
a, v, **kwargs
|
||||
),
|
||||
}
|
||||
self._write_fc = (0x05, 0x06, 0x0F, 0x10)
|
||||
|
||||
def __extract_result(self, fc_as_hex, result):
|
||||
"""Extract the values out of a response.
|
||||
|
||||
TODO make this consistent (values?)
|
||||
"""
|
||||
if not result.isError():
|
||||
if fc_as_hex in {"d", "c"}:
|
||||
return result.bits
|
||||
if fc_as_hex in {"h", "i"}:
|
||||
return result.registers
|
||||
else:
|
||||
return result
|
||||
return None
|
||||
785
env/lib/python3.11/site-packages/pymodbus/datastore/simulator.py
vendored
Normal file
785
env/lib/python3.11/site-packages/pymodbus/datastore/simulator.py
vendored
Normal file
@@ -0,0 +1,785 @@
|
||||
"""Pymodbus ModbusSimulatorContext."""
|
||||
import dataclasses
|
||||
import random
|
||||
import struct
|
||||
from datetime import datetime
|
||||
from typing import Any, Callable, Dict, List
|
||||
|
||||
|
||||
WORD_SIZE = 16
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class CellType:
|
||||
"""Define single cell types"""
|
||||
|
||||
INVALID: int = 0
|
||||
BITS: int = 1
|
||||
UINT16: int = 2
|
||||
UINT32: int = 3
|
||||
FLOAT32: int = 4
|
||||
STRING: int = 5
|
||||
NEXT: int = 6
|
||||
|
||||
|
||||
@dataclasses.dataclass(repr=False, eq=False)
|
||||
class Cell:
|
||||
"""Handle a single cell."""
|
||||
|
||||
type: int = CellType.INVALID
|
||||
access: bool = False
|
||||
value: int = 0
|
||||
action: int = 0
|
||||
action_kwargs: Dict[str, Any] = None
|
||||
count_read: int = 0
|
||||
count_write: int = 0
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Label: # pylint: disable=too-many-instance-attributes
|
||||
"""Defines all dict values.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
|
||||
action: str = "action"
|
||||
addr: str = "addr"
|
||||
any: str = "any"
|
||||
co_size: str = "co size"
|
||||
defaults: str = "defaults"
|
||||
di_size: str = "di size"
|
||||
hr_size: str = "hr size"
|
||||
increment: str = "increment"
|
||||
invalid: str = "invalid"
|
||||
ir_size: str = "ir size"
|
||||
kwargs: str = "kwargs"
|
||||
method: str = "method"
|
||||
next: str = "next"
|
||||
none: str = "none"
|
||||
random: str = "random"
|
||||
repeat: str = "repeat"
|
||||
reset: str = "reset"
|
||||
setup: str = "setup"
|
||||
shared_blocks: str = "shared blocks"
|
||||
timestamp: str = "timestamp"
|
||||
repeat_to: str = "to"
|
||||
type: str = "type"
|
||||
type_bits = "bits"
|
||||
type_exception: str = "type exception"
|
||||
type_uint16: str = "uint16"
|
||||
type_uint32: str = "uint32"
|
||||
type_float32: str = "float32"
|
||||
type_string: str = "string"
|
||||
uptime: str = "uptime"
|
||||
value: str = "value"
|
||||
write: str = "write"
|
||||
|
||||
@classmethod
|
||||
def try_get(cls, key, config_part):
|
||||
"""Check if entry is present in config."""
|
||||
if key not in config_part:
|
||||
txt = f"ERROR Configuration invalid, missing {key} in {config_part}"
|
||||
raise RuntimeError(txt)
|
||||
return config_part[key]
|
||||
|
||||
|
||||
class Setup:
|
||||
"""Setup simulator.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
|
||||
def __init__(self, runtime):
|
||||
"""Initialize."""
|
||||
self.runtime = runtime
|
||||
self.config = None
|
||||
self.config_types = {
|
||||
Label.type_bits: {
|
||||
Label.type: CellType.BITS,
|
||||
Label.next: None,
|
||||
Label.value: 0,
|
||||
Label.action: None,
|
||||
Label.method: self.handle_type_bits,
|
||||
},
|
||||
Label.type_uint16: {
|
||||
Label.type: CellType.UINT16,
|
||||
Label.next: None,
|
||||
Label.value: 0,
|
||||
Label.action: None,
|
||||
Label.method: self.handle_type_uint16,
|
||||
},
|
||||
Label.type_uint32: {
|
||||
Label.type: CellType.UINT32,
|
||||
Label.next: CellType.NEXT,
|
||||
Label.value: 0,
|
||||
Label.action: None,
|
||||
Label.method: self.handle_type_uint32,
|
||||
},
|
||||
Label.type_float32: {
|
||||
Label.type: CellType.FLOAT32,
|
||||
Label.next: CellType.NEXT,
|
||||
Label.value: 0,
|
||||
Label.action: None,
|
||||
Label.method: self.handle_type_float32,
|
||||
},
|
||||
Label.type_string: {
|
||||
Label.type: CellType.STRING,
|
||||
Label.next: CellType.NEXT,
|
||||
Label.value: 0,
|
||||
Label.action: None,
|
||||
Label.method: self.handle_type_string,
|
||||
},
|
||||
}
|
||||
|
||||
def handle_type_bits(self, start, stop, value, action, action_kwargs):
|
||||
"""Handle type bits."""
|
||||
for reg in self.runtime.registers[start:stop]:
|
||||
if reg.type != CellType.INVALID:
|
||||
raise RuntimeError(f'ERROR "{Label.type_bits}" {reg} used')
|
||||
reg.value = value
|
||||
reg.type = CellType.BITS
|
||||
reg.action = action
|
||||
reg.action_kwargs = action_kwargs
|
||||
|
||||
def handle_type_uint16(self, start, stop, value, action, action_kwargs):
|
||||
"""Handle type uint16."""
|
||||
for reg in self.runtime.registers[start:stop]:
|
||||
if reg.type != CellType.INVALID:
|
||||
raise RuntimeError(f'ERROR "{Label.type_uint16}" {reg} used')
|
||||
reg.value = value
|
||||
reg.type = CellType.UINT16
|
||||
reg.action = action
|
||||
reg.action_kwargs = action_kwargs
|
||||
|
||||
def handle_type_uint32(self, start, stop, value, action, action_kwargs):
|
||||
"""Handle type uint32."""
|
||||
regs_value = ModbusSimulatorContext.build_registers_from_value(value, True)
|
||||
for i in range(start, stop, 2):
|
||||
regs = self.runtime.registers[i : i + 2]
|
||||
if regs[0].type != CellType.INVALID or regs[1].type != CellType.INVALID:
|
||||
raise RuntimeError(f'ERROR "{Label.type_uint32}" {i},{i + 1} used')
|
||||
regs[0].value = regs_value[0]
|
||||
regs[0].type = CellType.UINT32
|
||||
regs[0].action = action
|
||||
regs[0].action_kwargs = action_kwargs
|
||||
regs[1].value = regs_value[1]
|
||||
regs[1].type = CellType.NEXT
|
||||
|
||||
def handle_type_float32(self, start, stop, value, action, action_kwargs):
|
||||
"""Handle type uint32."""
|
||||
regs_value = ModbusSimulatorContext.build_registers_from_value(value, False)
|
||||
for i in range(start, stop, 2):
|
||||
regs = self.runtime.registers[i : i + 2]
|
||||
if regs[0].type != CellType.INVALID or regs[1].type != CellType.INVALID:
|
||||
raise RuntimeError(f'ERROR "{Label.type_float32}" {i},{i + 1} used')
|
||||
regs[0].value = regs_value[0]
|
||||
regs[0].type = CellType.FLOAT32
|
||||
regs[0].action = action
|
||||
regs[0].action_kwargs = action_kwargs
|
||||
regs[1].value = regs_value[1]
|
||||
regs[1].type = CellType.NEXT
|
||||
|
||||
def handle_type_string(self, start, stop, value, action, action_kwargs):
|
||||
"""Handle type string."""
|
||||
regs = stop - start
|
||||
reg_len = regs * 2
|
||||
if len(value) > reg_len:
|
||||
raise RuntimeError(
|
||||
f'ERROR "{Label.type_string}" {start} too long "{value}"'
|
||||
)
|
||||
value = value.ljust(reg_len)
|
||||
for i in range(stop - start):
|
||||
reg = self.runtime.registers[start + i]
|
||||
if reg.type != CellType.INVALID:
|
||||
raise RuntimeError(f'ERROR "{Label.type_string}" {start + i} used')
|
||||
j = i * 2
|
||||
reg.value = int.from_bytes(bytes(value[j : j + 2], "UTF-8"), "big")
|
||||
reg.type = CellType.NEXT
|
||||
self.runtime.registers[start].type = CellType.STRING
|
||||
self.runtime.registers[start].action = action
|
||||
self.runtime.registers[start].action_kwargs = action_kwargs
|
||||
|
||||
def handle_setup_section(self):
|
||||
"""Load setup section"""
|
||||
layout = Label.try_get(Label.setup, self.config)
|
||||
self.runtime.fc_offset = {key: 0 for key in range(25)}
|
||||
size_co = Label.try_get(Label.co_size, layout)
|
||||
size_di = Label.try_get(Label.di_size, layout)
|
||||
size_hr = Label.try_get(Label.hr_size, layout)
|
||||
size_ir = Label.try_get(Label.ir_size, layout)
|
||||
if Label.try_get(Label.shared_blocks, layout):
|
||||
total_size = max(size_co, size_di, size_hr, size_ir)
|
||||
else:
|
||||
# set offset (block) for each function code
|
||||
# starting with fc = 1, 5, 15
|
||||
self.runtime.fc_offset[2] = size_co
|
||||
total_size = size_co + size_di
|
||||
self.runtime.fc_offset[4] = total_size
|
||||
total_size += size_ir
|
||||
for i in (3, 6, 16, 22, 23):
|
||||
self.runtime.fc_offset[i] = total_size
|
||||
total_size += size_hr
|
||||
first_cell = Cell()
|
||||
self.runtime.registers = [
|
||||
dataclasses.replace(first_cell) for i in range(total_size)
|
||||
]
|
||||
self.runtime.register_count = total_size
|
||||
self.runtime.type_exception = bool(Label.try_get(Label.type_exception, layout))
|
||||
defaults = Label.try_get(Label.defaults, layout)
|
||||
defaults_value = Label.try_get(Label.value, defaults)
|
||||
defaults_action = Label.try_get(Label.action, defaults)
|
||||
for key, entry in self.config_types.items():
|
||||
entry[Label.value] = Label.try_get(key, defaults_value)
|
||||
if (
|
||||
action := Label.try_get(key, defaults_action)
|
||||
) not in self.runtime.action_name_to_id:
|
||||
raise RuntimeError(f"ERROR illegal action {key} in {defaults_action}")
|
||||
entry[Label.action] = action
|
||||
del self.config[Label.setup]
|
||||
|
||||
def handle_invalid_address(self):
|
||||
"""Handle invalid address"""
|
||||
for entry in Label.try_get(Label.invalid, self.config):
|
||||
if isinstance(entry, int):
|
||||
entry = [entry, entry]
|
||||
for i in range(entry[0], entry[1] + 1):
|
||||
if i >= self.runtime.register_count:
|
||||
raise RuntimeError(
|
||||
f'Error section "{Label.invalid}" addr {entry} out of range'
|
||||
)
|
||||
reg = self.runtime.registers[i]
|
||||
reg.type = CellType.INVALID
|
||||
del self.config[Label.invalid]
|
||||
|
||||
def handle_write_allowed(self):
|
||||
"""Handle write allowed"""
|
||||
for entry in Label.try_get(Label.write, self.config):
|
||||
if isinstance(entry, int):
|
||||
entry = [entry, entry]
|
||||
for i in range(entry[0], entry[1] + 1):
|
||||
if i >= self.runtime.register_count:
|
||||
raise RuntimeError(
|
||||
f'Error section "{Label.write}" addr {entry} out of range'
|
||||
)
|
||||
reg = self.runtime.registers[i]
|
||||
if reg.type == CellType.INVALID:
|
||||
txt = f'ERROR Configuration invalid in section "write" register {i} not defined'
|
||||
raise RuntimeError(txt)
|
||||
reg.access = True
|
||||
del self.config[Label.write]
|
||||
|
||||
def handle_types(self):
|
||||
"""Handle the different types"""
|
||||
for section, type_entry in self.config_types.items():
|
||||
layout = Label.try_get(section, self.config)
|
||||
for entry in layout:
|
||||
if not isinstance(entry, dict):
|
||||
entry = {Label.addr: entry}
|
||||
regs = Label.try_get(Label.addr, entry)
|
||||
if not isinstance(regs, list):
|
||||
regs = [regs, regs]
|
||||
start = regs[0]
|
||||
if (stop := regs[1]) >= self.runtime.register_count:
|
||||
raise RuntimeError(f'Error "{section}" {start}, {stop} illegal')
|
||||
type_entry[Label.method](
|
||||
start,
|
||||
stop + 1,
|
||||
entry.get(Label.value, type_entry[Label.value]),
|
||||
self.runtime.action_name_to_id[
|
||||
entry.get(Label.action, type_entry[Label.action])
|
||||
],
|
||||
entry.get(Label.kwargs, None),
|
||||
)
|
||||
del self.config[section]
|
||||
|
||||
def handle_repeat(self):
|
||||
"""Handle repeat."""
|
||||
for entry in Label.try_get(Label.repeat, self.config):
|
||||
addr = Label.try_get(Label.addr, entry)
|
||||
copy_start = addr[0]
|
||||
copy_end = addr[1]
|
||||
copy_inx = copy_start - 1
|
||||
addr_to = Label.try_get(Label.repeat_to, entry)
|
||||
for inx in range(addr_to[0], addr_to[1] + 1):
|
||||
copy_inx = copy_start if copy_inx >= copy_end else copy_inx + 1
|
||||
if inx >= self.runtime.register_count:
|
||||
raise RuntimeError(
|
||||
f'Error section "{Label.repeat}" entry {entry} out of range'
|
||||
)
|
||||
self.runtime.registers[inx] = dataclasses.replace(
|
||||
self.runtime.registers[copy_inx]
|
||||
)
|
||||
del self.config[Label.repeat]
|
||||
|
||||
def setup(self, config, custom_actions) -> None:
|
||||
"""Load layout from dict with json structure."""
|
||||
actions = {
|
||||
Label.increment: self.runtime.action_increment,
|
||||
Label.random: self.runtime.action_random,
|
||||
Label.reset: self.runtime.action_reset,
|
||||
Label.timestamp: self.runtime.action_timestamp,
|
||||
Label.uptime: self.runtime.action_uptime,
|
||||
}
|
||||
if custom_actions:
|
||||
actions.update(custom_actions)
|
||||
self.runtime.action_name_to_id = {None: 0}
|
||||
self.runtime.action_id_to_name = [Label.none]
|
||||
self.runtime.action_methods = [None]
|
||||
i = 1
|
||||
for key, method in actions.items():
|
||||
self.runtime.action_name_to_id[key] = i
|
||||
self.runtime.action_id_to_name.append(key)
|
||||
self.runtime.action_methods.append(method)
|
||||
i += 1
|
||||
self.runtime.registerType_name_to_id = {
|
||||
Label.type_bits: CellType.BITS,
|
||||
Label.type_uint16: CellType.UINT16,
|
||||
Label.type_uint32: CellType.UINT32,
|
||||
Label.type_float32: CellType.FLOAT32,
|
||||
Label.type_string: CellType.STRING,
|
||||
Label.next: CellType.NEXT,
|
||||
Label.invalid: CellType.INVALID,
|
||||
}
|
||||
self.runtime.registerType_id_to_name = [None] * len(
|
||||
self.runtime.registerType_name_to_id
|
||||
)
|
||||
for name, cell_type in self.runtime.registerType_name_to_id.items():
|
||||
self.runtime.registerType_id_to_name[cell_type] = name
|
||||
|
||||
self.config = config
|
||||
self.handle_setup_section()
|
||||
self.handle_invalid_address()
|
||||
self.handle_types()
|
||||
self.handle_write_allowed()
|
||||
self.handle_repeat()
|
||||
if self.config:
|
||||
raise RuntimeError(f"INVALID key in setup: {self.config}")
|
||||
|
||||
|
||||
class ModbusSimulatorContext:
|
||||
"""Modbus simulator
|
||||
|
||||
:param config: A dict with structure as shown below.
|
||||
:param actions: A dict with "<name>": <function> structure.
|
||||
:raises RuntimeError: if json contains errors (msg explains what)
|
||||
|
||||
It builds and maintains a virtual copy of a device, with simulation of
|
||||
device specific functions.
|
||||
|
||||
The device is described in a dict, user supplied actions will
|
||||
be added to the builtin actions.
|
||||
|
||||
It is used in conjunction with a pymodbus server.
|
||||
|
||||
Example::
|
||||
|
||||
store = ModbusSimulatorContext(<config dict>, <actions dict>)
|
||||
StartAsyncTcpServer(<host>, context=store)
|
||||
|
||||
Now the server will simulate the defined device with features like:
|
||||
|
||||
- invalid addresses
|
||||
- write protected addresses
|
||||
- optional control of access for string, uint32, bit/bits
|
||||
- builtin actions for e.g. reset/datetime, value increment by read
|
||||
- custom actions
|
||||
|
||||
Description of the json file or dict to be supplied::
|
||||
|
||||
{
|
||||
"setup": {
|
||||
"di size": 0, --> Size of discrete input block (8 bit)
|
||||
"co size": 0, --> Size of coils block (8 bit)
|
||||
"ir size": 0, --> Size of input registers block (16 bit)
|
||||
"hr size": 0, --> Size of holding registers block (16 bit)
|
||||
"shared blocks": True, --> share memory for all blocks (largest size wins)
|
||||
"defaults": {
|
||||
"value": { --> Initial values (can be overwritten)
|
||||
"bits": 0x01,
|
||||
"uint16": 122,
|
||||
"uint32": 67000,
|
||||
"float32": 127.4,
|
||||
"string": " ",
|
||||
},
|
||||
"action": { --> default action (can be overwritten)
|
||||
"bits": None,
|
||||
"uint16": None,
|
||||
"uint32": None,
|
||||
"float32": None,
|
||||
"string": None,
|
||||
},
|
||||
},
|
||||
"type exception": False, --> return IO exception if read/write on non boundary
|
||||
},
|
||||
"invalid": [ --> List of invalid addresses, IO exception returned
|
||||
51, --> single register
|
||||
[78, 99], --> start, end registers, repeated as needed
|
||||
],
|
||||
"write": [ --> allow write, efault is ReadOnly
|
||||
[5, 5] --> start, end bytes, repeated as needed
|
||||
],
|
||||
"bits": [ --> Define bits (1 register == 1 byte)
|
||||
[30, 31], --> start, end registers, repeated as needed
|
||||
{"addr": [32, 34], "value": 0xF1}, --> with value
|
||||
{"addr": [35, 36], "action": "increment"}, --> with action
|
||||
{"addr": [37, 38], "action": "increment", "value": 0xF1} --> with action and value
|
||||
{"addr": [37, 38], "action": "increment", "kwargs": {"min": 0, "max": 100}} --> with action with arguments
|
||||
],
|
||||
"uint16": [ --> Define uint16 (1 register == 2 bytes)
|
||||
--> same as type_bits
|
||||
],
|
||||
"uint32": [ --> Define 32 bit integers (2 registers == 4 bytes)
|
||||
--> same as type_bits
|
||||
],
|
||||
"float32": [ --> Define 32 bit floats (2 registers == 4 bytes)
|
||||
--> same as type_bits
|
||||
],
|
||||
"string": [ --> Define strings (variable number of registers (each 2 bytes))
|
||||
[21, 22], --> start, end registers, define 1 string
|
||||
{"addr": 23, 25], "value": "ups"}, --> with value
|
||||
{"addr": 26, 27], "action": "user"}, --> with action
|
||||
{"addr": 28, 29], "action": "", "value": "user"} --> with action and value
|
||||
],
|
||||
"repeat": [ --> allows to repeat section e.g. for n devices
|
||||
{"addr": [100, 200], "to": [50, 275]} --> Repeat registers 100-200 to 50+ until 275
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
# --------------------------------------------
|
||||
# External interfaces
|
||||
# --------------------------------------------
|
||||
start_time = int(datetime.now().timestamp())
|
||||
|
||||
def __init__(
|
||||
self, config: Dict[str, Any], custom_actions: Dict[str, Callable]
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
self.registers: List[int] = []
|
||||
self.fc_offset: Dict[int, int] = {}
|
||||
self.register_count = 0
|
||||
self.type_exception = False
|
||||
self.action_name_to_id: Dict[str, int] = {}
|
||||
self.action_id_to_name: List[str] = []
|
||||
self.action_methods: List[Callable] = []
|
||||
self.registerType_name_to_id: Dict[str, int] = {}
|
||||
self.registerType_id_to_name: List[str] = []
|
||||
Setup(self).setup(config, custom_actions)
|
||||
|
||||
# --------------------------------------------
|
||||
# Simulator server interface
|
||||
# --------------------------------------------
|
||||
def get_text_register(self, register):
|
||||
"""Get raw register."""
|
||||
reg = self.registers[register]
|
||||
text_cell = Cell()
|
||||
text_cell.type = self.registerType_id_to_name[reg.type]
|
||||
text_cell.access = str(reg.access)
|
||||
text_cell.count_read = str(reg.count_read)
|
||||
text_cell.count_write = str(reg.count_write)
|
||||
text_cell.action = self.action_id_to_name[reg.action]
|
||||
if reg.action_kwargs:
|
||||
text_cell.action = f"{text_cell.action}({reg.action_kwargs})"
|
||||
if reg.type in (CellType.INVALID, CellType.UINT16, CellType.NEXT):
|
||||
text_cell.value = str(reg.value)
|
||||
build_len = 0
|
||||
elif reg.type == CellType.BITS:
|
||||
text_cell.value = hex(reg.value)
|
||||
build_len = 0
|
||||
elif reg.type == CellType.UINT32:
|
||||
tmp_regs = [reg.value, self.registers[register + 1].value]
|
||||
text_cell.value = str(self.build_value_from_registers(tmp_regs, True))
|
||||
build_len = 1
|
||||
elif reg.type == CellType.FLOAT32:
|
||||
tmp_regs = [reg.value, self.registers[register + 1].value]
|
||||
text_cell.value = str(self.build_value_from_registers(tmp_regs, False))
|
||||
build_len = 1
|
||||
else: # reg.type == CellType.STRING:
|
||||
j = register
|
||||
text_cell.value = ""
|
||||
while True:
|
||||
text_cell.value += str(
|
||||
self.registers[j].value.to_bytes(2, "big"),
|
||||
encoding="utf-8",
|
||||
errors="ignore",
|
||||
)
|
||||
j += 1
|
||||
if self.registers[j].type != CellType.NEXT:
|
||||
break
|
||||
build_len = j - register - 1
|
||||
reg_txt = f"{register}-{register + build_len}" if build_len else f"{register}"
|
||||
return reg_txt, text_cell
|
||||
|
||||
# --------------------------------------------
|
||||
# Modbus server interface
|
||||
# --------------------------------------------
|
||||
|
||||
_write_func_code = (5, 6, 15, 16, 22, 23)
|
||||
_bits_func_code = (1, 2, 5, 15)
|
||||
|
||||
def loop_validate(self, address, end_address, fx_write):
|
||||
"""Validate entry in loop.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
i = address
|
||||
while i < end_address:
|
||||
reg = self.registers[i]
|
||||
if fx_write and not reg.access or reg.type == CellType.INVALID:
|
||||
return False
|
||||
if not self.type_exception:
|
||||
i += 1
|
||||
continue
|
||||
if reg.type == CellType.NEXT:
|
||||
return False
|
||||
if reg.type in (CellType.BITS, CellType.UINT16):
|
||||
i += 1
|
||||
elif reg.type in (CellType.UINT32, CellType.FLOAT32):
|
||||
if i + 1 >= end_address:
|
||||
return False
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
while i < end_address:
|
||||
if self.registers[i].type == CellType.NEXT:
|
||||
i += 1
|
||||
return True
|
||||
|
||||
def validate(self, func_code, address, count=1):
|
||||
"""Check to see if the request is in range.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
if func_code in self._bits_func_code:
|
||||
# Bit count, correct to register count
|
||||
count = int((count + WORD_SIZE - 1) / WORD_SIZE)
|
||||
address = int(address / 16)
|
||||
|
||||
real_address = self.fc_offset[func_code] + address
|
||||
if real_address < 0 or real_address > self.register_count:
|
||||
return False
|
||||
|
||||
fx_write = func_code in self._write_func_code
|
||||
return self.loop_validate(real_address, real_address + count, fx_write)
|
||||
|
||||
def getValues(self, func_code, address, count=1):
|
||||
"""Return the requested values of the datastore.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
result = []
|
||||
if func_code not in self._bits_func_code:
|
||||
real_address = self.fc_offset[func_code] + address
|
||||
for i in range(real_address, real_address + count):
|
||||
reg = self.registers[i]
|
||||
kwargs = reg.action_kwargs if reg.action_kwargs else {}
|
||||
if reg.action:
|
||||
self.action_methods[reg.action](self.registers, i, reg, **kwargs)
|
||||
self.registers[i].count_read += 1
|
||||
result.append(reg.value)
|
||||
else:
|
||||
# bit access
|
||||
real_address = self.fc_offset[func_code] + int(address / 16)
|
||||
bit_index = address % 16
|
||||
reg_count = int((count + bit_index + 15) / 16)
|
||||
for i in range(real_address, real_address + reg_count):
|
||||
reg = self.registers[i]
|
||||
if reg.action:
|
||||
self.action_methods[reg.action](
|
||||
self.registers, i, reg, reg.action_kwargs
|
||||
)
|
||||
self.registers[i].count_read += 1
|
||||
while count and bit_index < 16:
|
||||
result.append(bool(reg.value & (2**bit_index)))
|
||||
count -= 1
|
||||
bit_index += 1
|
||||
bit_index = 0
|
||||
return result
|
||||
|
||||
def setValues(self, func_code, address, values):
|
||||
"""Set the requested values of the datastore.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
if func_code not in self._bits_func_code:
|
||||
real_address = self.fc_offset[func_code] + address
|
||||
for value in values:
|
||||
self.registers[real_address].value = value
|
||||
self.registers[real_address].count_write += 1
|
||||
real_address += 1
|
||||
return
|
||||
|
||||
# bit access
|
||||
real_address = self.fc_offset[func_code] + int(address / 16)
|
||||
bit_index = address % 16
|
||||
for value in values:
|
||||
bit_mask = 2**bit_index
|
||||
if bool(value):
|
||||
self.registers[real_address].value |= bit_mask
|
||||
else:
|
||||
self.registers[real_address].value &= ~bit_mask
|
||||
self.registers[real_address].count_write += 1
|
||||
bit_index += 1
|
||||
if bit_index == 16:
|
||||
bit_index = 0
|
||||
real_address += 1
|
||||
return
|
||||
|
||||
# --------------------------------------------
|
||||
# Internal action methods
|
||||
# --------------------------------------------
|
||||
|
||||
@classmethod
|
||||
def action_random(cls, registers, inx, cell, minval=1, maxval=65536):
|
||||
"""Update with random value.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
if cell.type in (CellType.BITS, CellType.UINT16):
|
||||
registers[inx].value = random.randint(int(minval), int(maxval))
|
||||
elif cell.type == CellType.FLOAT32:
|
||||
regs = cls.build_registers_from_value(
|
||||
random.uniform(float(minval), float(maxval)), False
|
||||
)
|
||||
registers[inx].value = regs[0]
|
||||
registers[inx + 1].value = regs[1]
|
||||
elif cell.type == CellType.UINT32:
|
||||
regs = cls.build_registers_from_value(
|
||||
random.randint(int(minval), int(maxval)), True
|
||||
)
|
||||
registers[inx].value = regs[0]
|
||||
registers[inx + 1].value = regs[1]
|
||||
|
||||
@classmethod
|
||||
def action_increment(cls, registers, inx, cell, minval=None, maxval=None):
|
||||
"""Increment value reset with overflow.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
reg = registers[inx]
|
||||
reg2 = registers[inx + 1]
|
||||
if cell.type in (CellType.BITS, CellType.UINT16):
|
||||
value = reg.value + 1
|
||||
if maxval and value > maxval:
|
||||
value = minval
|
||||
if minval and value < minval:
|
||||
value = minval
|
||||
reg.value = value
|
||||
elif cell.type == CellType.FLOAT32:
|
||||
tmp_reg = [reg.value, reg2.value]
|
||||
value = cls.build_value_from_registers(tmp_reg, False)
|
||||
value += 1.0
|
||||
if maxval and value > maxval:
|
||||
value = minval
|
||||
if minval and value < minval:
|
||||
value = minval
|
||||
new_regs = cls.build_registers_from_value(value, False)
|
||||
reg.value = new_regs[0]
|
||||
reg2.value = new_regs[1]
|
||||
elif cell.type == CellType.UINT32:
|
||||
tmp_reg = [reg.value, reg2.value]
|
||||
value = cls.build_value_from_registers(tmp_reg, True)
|
||||
value += 1
|
||||
if maxval and value > maxval:
|
||||
value = minval
|
||||
if minval and value < minval:
|
||||
value = minval
|
||||
new_regs = cls.build_registers_from_value(value, True)
|
||||
reg.value = new_regs[0]
|
||||
reg2.value = new_regs[1]
|
||||
|
||||
@classmethod
|
||||
def action_timestamp(cls, registers, inx, _cell, **_kwargs):
|
||||
"""Set current time.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
system_time = datetime.now()
|
||||
registers[inx].value = system_time.year
|
||||
registers[inx + 1].value = system_time.month - 1
|
||||
registers[inx + 2].value = system_time.day
|
||||
registers[inx + 3].value = system_time.weekday() + 1
|
||||
registers[inx + 4].value = system_time.hour
|
||||
registers[inx + 5].value = system_time.minute
|
||||
registers[inx + 6].value = system_time.second
|
||||
|
||||
@classmethod
|
||||
def action_reset(cls, _registers, _inx, _cell, **_kwargs):
|
||||
"""Reboot server.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
raise RuntimeError("RESET server")
|
||||
|
||||
@classmethod
|
||||
def action_uptime(cls, registers, inx, cell, **_kwargs):
|
||||
"""Return uptime in seconds.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
value = int(datetime.now().timestamp()) - cls.start_time + 1
|
||||
|
||||
if cell.type in (CellType.BITS, CellType.UINT16):
|
||||
registers[inx].value = value
|
||||
elif cell.type == CellType.FLOAT32:
|
||||
regs = cls.build_registers_from_value(value, False)
|
||||
registers[inx].value = regs[0]
|
||||
registers[inx + 1].value = regs[1]
|
||||
elif cell.type == CellType.UINT32:
|
||||
regs = cls.build_registers_from_value(value, True)
|
||||
registers[inx].value = regs[0]
|
||||
registers[inx + 1].value = regs[1]
|
||||
|
||||
# --------------------------------------------
|
||||
# Internal helper methods
|
||||
# --------------------------------------------
|
||||
|
||||
def validate_type(self, func_code, real_address, count):
|
||||
"""Check if request is done against correct type
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
|
||||
if func_code in self._bits_func_code:
|
||||
# Bit access
|
||||
check = (CellType.BITS, -1)
|
||||
reg_step = 1
|
||||
elif count % 2:
|
||||
# 16 bit access
|
||||
check = (CellType.UINT16, CellType.STRING)
|
||||
reg_step = 1
|
||||
else:
|
||||
check = (CellType.UINT32, CellType.FLOAT32, CellType.STRING)
|
||||
reg_step = 2
|
||||
|
||||
for i in range(real_address, real_address + count, reg_step):
|
||||
if self.registers[i].type in check:
|
||||
continue
|
||||
if self.registers[i].type is CellType.NEXT:
|
||||
continue
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def build_registers_from_value(cls, value, is_int):
|
||||
"""Build registers from int32 or float32"""
|
||||
regs = [0, 0]
|
||||
if is_int:
|
||||
value_bytes = int.to_bytes(value, 4, "big")
|
||||
else:
|
||||
value_bytes = struct.pack(">f", value)
|
||||
regs[0] = int.from_bytes(value_bytes[:2], "big")
|
||||
regs[1] = int.from_bytes(value_bytes[-2:], "big")
|
||||
return regs
|
||||
|
||||
@classmethod
|
||||
def build_value_from_registers(cls, registers, is_int):
|
||||
"""Build int32 or float32 value from registers"""
|
||||
value_bytes = int.to_bytes(registers[0], 2, "big") + int.to_bytes(
|
||||
registers[1], 2, "big"
|
||||
)
|
||||
if is_int:
|
||||
value = int.from_bytes(value_bytes, "big")
|
||||
else:
|
||||
value = struct.unpack(">f", value_bytes)[0]
|
||||
return value
|
||||
314
env/lib/python3.11/site-packages/pymodbus/datastore/store.py
vendored
Normal file
314
env/lib/python3.11/site-packages/pymodbus/datastore/store.py
vendored
Normal file
@@ -0,0 +1,314 @@
|
||||
"""Modbus Server Datastore.
|
||||
|
||||
For each server, you will create a ModbusServerContext and pass
|
||||
in the default address space for each data access. The class
|
||||
will create and manage the data.
|
||||
|
||||
Further modification of said data accesses should be performed
|
||||
with [get,set][access]Values(address, count)
|
||||
|
||||
Datastore Implementation
|
||||
-------------------------
|
||||
|
||||
There are two ways that the server datastore can be implemented.
|
||||
The first is a complete range from "address" start to "count"
|
||||
number of indices. This can be thought of as a straight array::
|
||||
|
||||
data = range(1, 1 + count)
|
||||
[1,2,3,...,count]
|
||||
|
||||
The other way that the datastore can be implemented (and how
|
||||
many devices implement it) is a associate-array::
|
||||
|
||||
data = {1:"1", 3:"3", ..., count:"count"}
|
||||
[1,3,...,count]
|
||||
|
||||
The difference between the two is that the latter will allow
|
||||
arbitrary gaps in its datastore while the former will not.
|
||||
This is seen quite commonly in some modbus implementations.
|
||||
What follows is a clear example from the field:
|
||||
|
||||
Say a company makes two devices to monitor power usage on a rack.
|
||||
One works with three-phase and the other with a single phase. The
|
||||
company will dictate a modbus data mapping such that registers::
|
||||
|
||||
n: phase 1 power
|
||||
n+1: phase 2 power
|
||||
n+2: phase 3 power
|
||||
|
||||
Using this, layout, the first device will implement n, n+1, and n+2,
|
||||
however, the second device may set the latter two values to 0 or
|
||||
will simply not implemented the registers thus causing a single read
|
||||
or a range read to fail.
|
||||
|
||||
I have both methods implemented, and leave it up to the user to change
|
||||
based on their preference.
|
||||
"""
|
||||
# pylint: disable=missing-type-doc
|
||||
from pymodbus.exceptions import NotImplementedException, ParameterException
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Datablock Storage
|
||||
# ---------------------------------------------------------------------------#
|
||||
class BaseModbusDataBlock:
|
||||
"""Base class for a modbus datastore
|
||||
|
||||
Derived classes must create the following fields:
|
||||
@address The starting address point
|
||||
@defult_value The default value of the datastore
|
||||
@values The actual datastore values
|
||||
|
||||
Derived classes must implemented the following methods:
|
||||
validate(self, address, count=1)
|
||||
getValues(self, address, count=1)
|
||||
setValues(self, address, values)
|
||||
"""
|
||||
|
||||
def default(self, count, value=False):
|
||||
"""Use to initialize a store to one value.
|
||||
|
||||
:param count: The number of fields to set
|
||||
:param value: The default value to set to the fields
|
||||
"""
|
||||
self.default_value = value # pylint: disable=attribute-defined-outside-init
|
||||
self.values = [ # pylint: disable=attribute-defined-outside-init
|
||||
self.default_value
|
||||
] * count
|
||||
self.address = 0x00 # pylint: disable=attribute-defined-outside-init
|
||||
|
||||
def reset(self):
|
||||
"""Reset the datastore to the initialized default value."""
|
||||
self.values = [ # pylint: disable=attribute-defined-outside-init
|
||||
self.default_value
|
||||
] * len(self.values)
|
||||
|
||||
def validate(self, address, count=1):
|
||||
"""Check to see if the request is in range.
|
||||
|
||||
:param address: The starting address
|
||||
:param count: The number of values to test for
|
||||
:raises NotImplementedException:
|
||||
"""
|
||||
raise NotImplementedException("Datastore Address Check")
|
||||
|
||||
def getValues(self, address, count=1):
|
||||
"""Return the requested values from the datastore.
|
||||
|
||||
:param address: The starting address
|
||||
:param count: The number of values to retrieve
|
||||
:raises NotImplementedException:
|
||||
"""
|
||||
raise NotImplementedException("Datastore Value Retrieve")
|
||||
|
||||
def setValues(self, address, values):
|
||||
"""Return the requested values from the datastore.
|
||||
|
||||
:param address: The starting address
|
||||
:param values: The values to store
|
||||
:raises NotImplementedException:
|
||||
"""
|
||||
raise NotImplementedException("Datastore Value Retrieve")
|
||||
|
||||
def __str__(self):
|
||||
"""Build a representation of the datastore.
|
||||
|
||||
:returns: A string representation of the datastore
|
||||
"""
|
||||
return f"DataStore({len(self.values)}, {self.default_value})"
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over the data block data.
|
||||
|
||||
:returns: An iterator of the data block data
|
||||
"""
|
||||
if isinstance(self.values, dict):
|
||||
return iter(self.values.items())
|
||||
return enumerate(self.values, self.address)
|
||||
|
||||
|
||||
class ModbusSequentialDataBlock(BaseModbusDataBlock):
|
||||
"""Creates a sequential modbus datastore."""
|
||||
|
||||
def __init__(self, address, values):
|
||||
"""Initialize the datastore.
|
||||
|
||||
:param address: The starting address of the datastore
|
||||
:param values: Either a list or a dictionary of values
|
||||
"""
|
||||
self.address = address
|
||||
if hasattr(values, "__iter__"):
|
||||
self.values = list(values)
|
||||
else:
|
||||
self.values = [values]
|
||||
self.default_value = self.values[0].__class__()
|
||||
|
||||
@classmethod
|
||||
def create(cls):
|
||||
"""Create a datastore.
|
||||
|
||||
With the full address space initialized to 0x00
|
||||
|
||||
:returns: An initialized datastore
|
||||
"""
|
||||
return cls(0x00, [0x00] * 65536)
|
||||
|
||||
def validate(self, address, count=1):
|
||||
"""Check to see if the request is in range.
|
||||
|
||||
:param address: The starting address
|
||||
:param count: The number of values to test for
|
||||
:returns: True if the request in within range, False otherwise
|
||||
"""
|
||||
result = self.address <= address
|
||||
result &= (self.address + len(self.values)) >= (address + count)
|
||||
return result
|
||||
|
||||
def getValues(self, address, count=1):
|
||||
"""Return the requested values of the datastore.
|
||||
|
||||
:param address: The starting address
|
||||
:param count: The number of values to retrieve
|
||||
:returns: The requested values from a:a+c
|
||||
"""
|
||||
start = address - self.address
|
||||
return self.values[start : start + count]
|
||||
|
||||
def setValues(self, address, values):
|
||||
"""Set the requested values of the datastore.
|
||||
|
||||
:param address: The starting address
|
||||
:param values: The new values to be set
|
||||
"""
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
start = address - self.address
|
||||
self.values[start : start + len(values)] = values
|
||||
|
||||
|
||||
class ModbusSparseDataBlock(BaseModbusDataBlock):
|
||||
"""Create a sparse modbus datastore.
|
||||
|
||||
E.g Usage.
|
||||
sparse = ModbusSparseDataBlock({10: [3, 5, 6, 8], 30: 1, 40: [0]*20})
|
||||
|
||||
This would create a datablock with 3 blocks starting at
|
||||
offset 10 with length 4 , 30 with length 1 and 40 with length 20
|
||||
|
||||
sparse = ModbusSparseDataBlock([10]*100)
|
||||
Creates a sparse datablock of length 100 starting at offset 0 and default value of 10
|
||||
|
||||
sparse = ModbusSparseDataBlock() --> Create Empty datablock
|
||||
sparse.setValues(0, [10]*10) --> Add block 1 at offset 0 with length 10 (default value 10)
|
||||
sparse.setValues(30, [20]*5) --> Add block 2 at offset 30 with length 5 (default value 20)
|
||||
|
||||
if mutable is set to True during initialization, the datablock can not be altered with
|
||||
setValues (new datablocks can not be added)
|
||||
"""
|
||||
|
||||
def __init__(self, values=None, mutable=True):
|
||||
"""Initialize a sparse datastore.
|
||||
|
||||
Will only answer to addresses
|
||||
registered, either initially here, or later via setValues()
|
||||
|
||||
:param values: Either a list or a dictionary of values
|
||||
:param mutable: The data-block can be altered later with setValues(i.e add more blocks)
|
||||
|
||||
If values are list , This is as good as sequential datablock.
|
||||
Values as dictionary should be in {offset: <values>} format, if values
|
||||
is a list, a sparse datablock is created starting at offset with the length of values.
|
||||
If values is a integer, then the value is set for the corresponding offset.
|
||||
|
||||
"""
|
||||
self.values = {}
|
||||
self._process_values(values)
|
||||
self.mutable = mutable
|
||||
self.default_value = self.values.copy()
|
||||
self.address = next(iter(self.values.keys()), None)
|
||||
|
||||
@classmethod
|
||||
def create(cls, values=None):
|
||||
"""Create sparse datastore.
|
||||
|
||||
Use setValues to initialize registers.
|
||||
|
||||
:param values: Either a list or a dictionary of values
|
||||
:returns: An initialized datastore
|
||||
"""
|
||||
return cls(values)
|
||||
|
||||
def reset(self):
|
||||
"""Reset the store to the initially provided defaults."""
|
||||
self.values = self.default_value.copy()
|
||||
|
||||
def validate(self, address, count=1):
|
||||
"""Check to see if the request is in range.
|
||||
|
||||
:param address: The starting address
|
||||
:param count: The number of values to test for
|
||||
:returns: True if the request in within range, False otherwise
|
||||
"""
|
||||
if not count:
|
||||
return False
|
||||
handle = set(range(address, address + count))
|
||||
return handle.issubset(set(iter(self.values.keys())))
|
||||
|
||||
def getValues(self, address, count=1):
|
||||
"""Return the requested values of the datastore.
|
||||
|
||||
:param address: The starting address
|
||||
:param count: The number of values to retrieve
|
||||
:returns: The requested values from a:a+c
|
||||
"""
|
||||
return [self.values[i] for i in range(address, address + count)]
|
||||
|
||||
def _process_values(self, values):
|
||||
"""Process values."""
|
||||
|
||||
def _process_as_dict(values):
|
||||
for idx, val in iter(values.items()):
|
||||
if isinstance(val, (list, tuple)):
|
||||
for i, v_item in enumerate(val):
|
||||
self.values[idx + i] = v_item
|
||||
else:
|
||||
self.values[idx] = int(val)
|
||||
|
||||
if isinstance(values, dict):
|
||||
_process_as_dict(values)
|
||||
return
|
||||
if hasattr(values, "__iter__"):
|
||||
values = dict(enumerate(values))
|
||||
elif values is None:
|
||||
values = {} # Must make a new dict here per instance
|
||||
else:
|
||||
raise ParameterException(
|
||||
"Values for datastore must be a list or dictionary"
|
||||
)
|
||||
_process_as_dict(values)
|
||||
|
||||
def setValues(self, address, values, use_as_default=False):
|
||||
"""Set the requested values of the datastore.
|
||||
|
||||
:param address: The starting address
|
||||
:param values: The new values to be set
|
||||
:param use_as_default: Use the values as default
|
||||
:raises ParameterException:
|
||||
"""
|
||||
if isinstance(values, dict):
|
||||
new_offsets = list(set(values.keys()) - set(self.values.keys()))
|
||||
if new_offsets and not self.mutable:
|
||||
raise ParameterException(f"Offsets {new_offsets} not in range")
|
||||
self._process_values(values)
|
||||
else:
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
for idx, val in enumerate(values):
|
||||
if address + idx not in self.values and not self.mutable:
|
||||
raise ParameterException("Offset {address+idx} not in range")
|
||||
self.values[address + idx] = val
|
||||
if not self.address:
|
||||
self.address = next(iter(self.values.keys()), None)
|
||||
if use_as_default:
|
||||
for idx, val in iter(self.values.items()):
|
||||
self.default_value[idx] = val
|
||||
608
env/lib/python3.11/site-packages/pymodbus/device.py
vendored
Normal file
608
env/lib/python3.11/site-packages/pymodbus/device.py
vendored
Normal file
@@ -0,0 +1,608 @@
|
||||
"""Modbus Device Controller.
|
||||
|
||||
These are the device management handlers. They should be
|
||||
maintained in the server context and the various methods
|
||||
should be inserted in the correct locations.
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
"ModbusPlusStatistics",
|
||||
"ModbusDeviceIdentification",
|
||||
"DeviceInformationFactory",
|
||||
]
|
||||
|
||||
import struct
|
||||
|
||||
# pylint: disable=missing-type-doc
|
||||
from collections import OrderedDict
|
||||
from typing import List
|
||||
|
||||
from pymodbus.constants import INTERNAL_ERROR, DeviceInformation
|
||||
from pymodbus.events import ModbusEvent
|
||||
from pymodbus.utilities import dict_property
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Modbus Plus Statistics
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ModbusPlusStatistics:
|
||||
"""This is used to maintain the current modbus plus statistics count.
|
||||
|
||||
As of right now this is simply a stub to complete the modbus implementation.
|
||||
For more information, see the modbus implementation guide page 87.
|
||||
"""
|
||||
|
||||
__data = OrderedDict(
|
||||
{
|
||||
"node_type_id": [0x00] * 2, # 00
|
||||
"software_version_number": [0x00] * 2, # 01
|
||||
"network_address": [0x00] * 2, # 02
|
||||
"mac_state_variable": [0x00] * 2, # 03
|
||||
"peer_status_code": [0x00] * 2, # 04
|
||||
"token_pass_counter": [0x00] * 2, # 05
|
||||
"token_rotation_time": [0x00] * 2, # 06
|
||||
"program_master_token_failed": [0x00], # 07 hi
|
||||
"data_master_token_failed": [0x00], # 07 lo
|
||||
"program_master_token_owner": [0x00], # 08 hi
|
||||
"data_master_token_owner": [0x00], # 08 lo
|
||||
"program_slave_token_owner": [0x00], # 09 hi
|
||||
"data_slave_token_owner": [0x00], # 09 lo
|
||||
"data_slave_command_transfer": [0x00], # 10 hi
|
||||
"__unused_10_lowbit": [0x00], # 10 lo
|
||||
"program_slave_command_transfer": [0x00], # 11 hi
|
||||
"program_master_rsp_transfer": [0x00], # 11 lo
|
||||
"program_slave_auto_logout": [0x00], # 12 hi
|
||||
"program_master_connect_status": [0x00], # 12 lo
|
||||
"receive_buffer_dma_overrun": [0x00], # 13 hi
|
||||
"pretransmit_deferral_error": [0x00], # 13 lo
|
||||
"frame_size_error": [0x00], # 14 hi
|
||||
"repeated_command_received": [0x00], # 14 lo
|
||||
"receiver_alignment_error": [0x00], # 15 hi
|
||||
"receiver_collision_abort_error": [0x00], # 15 lo
|
||||
"bad_packet_length_error": [0x00], # 16 hi
|
||||
"receiver_crc_error": [0x00], # 16 lo
|
||||
"transmit_buffer_dma_underrun": [0x00], # 17 hi
|
||||
"bad_link_address_error": [0x00], # 17 lo
|
||||
"bad_mac_function_code_error": [0x00], # 18 hi
|
||||
"internal_packet_length_error": [0x00], # 18 lo
|
||||
"communication_failed_error": [0x00], # 19 hi
|
||||
"communication_retries": [0x00], # 19 lo
|
||||
"no_response_error": [0x00], # 20 hi
|
||||
"good_receive_packet": [0x00], # 20 lo
|
||||
"unexpected_path_error": [0x00], # 21 hi
|
||||
"exception_response_error": [0x00], # 21 lo
|
||||
"forgotten_transaction_error": [0x00], # 22 hi
|
||||
"unexpected_response_error": [0x00], # 22 lo
|
||||
"active_station_bit_map": [0x00] * 8, # 23-26
|
||||
"token_station_bit_map": [0x00] * 8, # 27-30
|
||||
"global_data_bit_map": [0x00] * 8, # 31-34
|
||||
"receive_buffer_use_bit_map": [0x00] * 8, # 35-37
|
||||
"data_master_output_path": [0x00] * 8, # 38-41
|
||||
"data_slave_input_path": [0x00] * 8, # 42-45
|
||||
"program_master_outptu_path": [0x00] * 8, # 46-49
|
||||
"program_slave_input_path": [0x00] * 8, # 50-53
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the modbus plus statistics with the default information."""
|
||||
self.reset()
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over the statistics.
|
||||
|
||||
:returns: An iterator of the modbus plus statistics
|
||||
"""
|
||||
return iter(self.__data.items())
|
||||
|
||||
def reset(self):
|
||||
"""Clear all of the modbus plus statistics."""
|
||||
for key in self.__data:
|
||||
self.__data[key] = [0x00] * len(self.__data[key])
|
||||
|
||||
def summary(self):
|
||||
"""Return a summary of the modbus plus statistics.
|
||||
|
||||
:returns: 54 16-bit words representing the status
|
||||
"""
|
||||
return iter(self.__data.values())
|
||||
|
||||
def encode(self):
|
||||
"""Return a summary of the modbus plus statistics.
|
||||
|
||||
:returns: 54 16-bit words representing the status
|
||||
"""
|
||||
total, values = [], sum(self.__data.values(), [])
|
||||
for i in range(0, len(values), 2):
|
||||
total.append((values[i] << 8) | values[i + 1])
|
||||
return total
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Device Information Control
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ModbusDeviceIdentification:
|
||||
"""This is used to supply the device identification.
|
||||
|
||||
For the readDeviceIdentification function
|
||||
|
||||
For more information read section 6.21 of the modbus
|
||||
application protocol.
|
||||
"""
|
||||
|
||||
__data = {
|
||||
0x00: "", # VendorName
|
||||
0x01: "", # ProductCode
|
||||
0x02: "", # MajorMinorRevision
|
||||
0x03: "", # VendorUrl
|
||||
0x04: "", # ProductName
|
||||
0x05: "", # ModelName
|
||||
0x06: "", # UserApplicationName
|
||||
0x07: "", # reserved
|
||||
0x08: "", # reserved
|
||||
# 0x80 -> 0xFF are privatek
|
||||
}
|
||||
|
||||
__names = [
|
||||
"VendorName",
|
||||
"ProductCode",
|
||||
"MajorMinorRevision",
|
||||
"VendorUrl",
|
||||
"ProductName",
|
||||
"ModelName",
|
||||
"UserApplicationName",
|
||||
]
|
||||
|
||||
def __init__(self, info=None, info_name=None):
|
||||
"""Initialize the datastore with the elements you need.
|
||||
|
||||
(note acceptable range is [0x00-0x06,0x80-0xFF] inclusive)
|
||||
|
||||
:param info: A dictionary of {int:string} of values
|
||||
:param set: A dictionary of {name:string} of values
|
||||
"""
|
||||
if isinstance(info_name, dict):
|
||||
for key in info_name:
|
||||
inx = self.__names.index(key)
|
||||
self.__data[inx] = info_name[key]
|
||||
|
||||
if isinstance(info, dict):
|
||||
for key in info:
|
||||
if (0x06 >= key >= 0x00) or (0xFF >= key >= 0x80):
|
||||
self.__data[key] = info[key]
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over the device information.
|
||||
|
||||
:returns: An iterator of the device information
|
||||
"""
|
||||
return iter(self.__data.items())
|
||||
|
||||
def summary(self):
|
||||
"""Return a summary of the main items.
|
||||
|
||||
:returns: An dictionary of the main items
|
||||
"""
|
||||
return dict(zip(self.__names, iter(self.__data.values())))
|
||||
|
||||
def update(self, value):
|
||||
"""Update the values of this identity.
|
||||
|
||||
using another identify as the value
|
||||
|
||||
:param value: The value to copy values from
|
||||
"""
|
||||
self.__data.update(value)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Access the device information.
|
||||
|
||||
:param key: The register to set
|
||||
:param value: The new value for referenced register
|
||||
"""
|
||||
if key not in [0x07, 0x08]:
|
||||
self.__data[key] = value
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Access the device information.
|
||||
|
||||
:param key: The register to read
|
||||
"""
|
||||
return self.__data.setdefault(key, "")
|
||||
|
||||
def __str__(self):
|
||||
"""Build a representation of the device.
|
||||
|
||||
:returns: A string representation of the device
|
||||
"""
|
||||
return "DeviceIdentity"
|
||||
|
||||
# -------------------------------------------------------------------------#
|
||||
# Properties
|
||||
# -------------------------------------------------------------------------#
|
||||
VendorName = dict_property(
|
||||
lambda s: s.__data, 0 # pylint: disable=protected-access
|
||||
)
|
||||
ProductCode = dict_property(
|
||||
lambda s: s.__data, 1 # pylint: disable=protected-access
|
||||
)
|
||||
MajorMinorRevision = dict_property(
|
||||
lambda s: s.__data, 2 # pylint: disable=protected-access
|
||||
)
|
||||
VendorUrl = dict_property(lambda s: s.__data, 3) # pylint: disable=protected-access
|
||||
ProductName = dict_property(
|
||||
lambda s: s.__data, 4 # pylint: disable=protected-access
|
||||
)
|
||||
ModelName = dict_property(lambda s: s.__data, 5) # pylint: disable=protected-access
|
||||
UserApplicationName = dict_property(
|
||||
lambda s: s.__data, 6 # pylint: disable=protected-access
|
||||
)
|
||||
|
||||
|
||||
class DeviceInformationFactory: # pylint: disable=too-few-public-methods
|
||||
"""This is a helper factory.
|
||||
|
||||
That really just hides
|
||||
some of the complexity of processing the device information
|
||||
requests (function code 0x2b 0x0e).
|
||||
"""
|
||||
|
||||
__lookup = {
|
||||
DeviceInformation.BASIC: lambda c, r, i: c.__gets( # pylint: disable=protected-access
|
||||
r, list(range(i, 0x03))
|
||||
),
|
||||
DeviceInformation.REGULAR: lambda c, r, i: c.__gets( # pylint: disable=protected-access
|
||||
r,
|
||||
list(range(i, 0x07))
|
||||
if c.__get(r, i)[i] # pylint: disable=protected-access
|
||||
else list(range(0, 0x07)),
|
||||
),
|
||||
DeviceInformation.EXTENDED: lambda c, r, i: c.__gets( # pylint: disable=protected-access
|
||||
r,
|
||||
[x for x in range(i, 0x100) if x not in range(0x07, 0x80)]
|
||||
if c.__get(r, i)[i] # pylint: disable=protected-access
|
||||
else [x for x in range(0, 0x100) if x not in range(0x07, 0x80)],
|
||||
),
|
||||
DeviceInformation.SPECIFIC: lambda c, r, i: c.__get( # pylint: disable=protected-access
|
||||
r, i
|
||||
),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get(cls, control, read_code=DeviceInformation.BASIC, object_id=0x00):
|
||||
"""Get the requested device data from the system.
|
||||
|
||||
:param control: The control block to pull data from
|
||||
:param read_code: The read code to process
|
||||
:param object_id: The specific object_id to read
|
||||
:returns: The requested data (id, length, value)
|
||||
"""
|
||||
identity = control.Identity
|
||||
return cls.__lookup[read_code](cls, identity, object_id)
|
||||
|
||||
@classmethod
|
||||
def __get(cls, identity, object_id): # pylint: disable=unused-private-member
|
||||
"""Read a single object_id from the device information.
|
||||
|
||||
:param identity: The identity block to pull data from
|
||||
:param object_id: The specific object id to read
|
||||
:returns: The requested data (id, length, value)
|
||||
"""
|
||||
return {object_id: identity[object_id]}
|
||||
|
||||
@classmethod
|
||||
def __gets(cls, identity, object_ids): # pylint: disable=unused-private-member
|
||||
"""Read multiple object_ids from the device information.
|
||||
|
||||
:param identity: The identity block to pull data from
|
||||
:param object_ids: The specific object ids to read
|
||||
:returns: The requested data (id, length, value)
|
||||
"""
|
||||
return {oid: identity[oid] for oid in object_ids if identity[oid]}
|
||||
|
||||
def __init__(self):
|
||||
"""Prohibit objects."""
|
||||
raise RuntimeError(INTERNAL_ERROR)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Counters Handler
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ModbusCountersHandler:
|
||||
"""This is a helper class to simplify the properties for the counters.
|
||||
|
||||
0x0B 1 Return Bus Message Count
|
||||
|
||||
Quantity of messages that the remote
|
||||
device has detected on the communications system since its
|
||||
last restart, clear counters operation, or power-up. Messages
|
||||
with bad CRC are not taken into account.
|
||||
|
||||
0x0C 2 Return Bus Communication Error Count
|
||||
|
||||
Quantity of CRC errors encountered by the remote device since its
|
||||
last restart, clear counters operation, or power-up. In case of
|
||||
an error detected on the character level, (overrun, parity error),
|
||||
or in case of a message length < 3 bytes, the receiving device is
|
||||
not able to calculate the CRC. In such cases, this counter is
|
||||
also incremented.
|
||||
|
||||
0x0D 3 Return Slave Exception Error Count
|
||||
|
||||
Quantity of MODBUS exception error detected by the remote device
|
||||
since its last restart, clear counters operation, or power-up. It
|
||||
comprises also the error detected in broadcast messages even if an
|
||||
exception message is not returned in this case.
|
||||
Exception errors are described and listed in "MODBUS Application
|
||||
Protocol Specification" document.
|
||||
|
||||
0xOE 4 Return Slave Message Count
|
||||
|
||||
Quantity of messages addressed to the remote device, including
|
||||
broadcast messages, that the remote device has processed since its
|
||||
last restart, clear counters operation, or power-up.
|
||||
|
||||
0x0F 5 Return Slave No Response Count
|
||||
|
||||
Quantity of messages received by the remote device for which it
|
||||
returned no response (neither a normal response nor an exception
|
||||
response), since its last restart, clear counters operation, or
|
||||
power-up. Then, this counter counts the number of broadcast
|
||||
messages it has received.
|
||||
|
||||
0x10 6 Return Slave NAK Count
|
||||
|
||||
Quantity of messages addressed to the remote device for which it
|
||||
returned a Negative Acknowledge (NAK) exception response, since
|
||||
its last restart, clear counters operation, or power-up. Exception
|
||||
responses are described and listed in "MODBUS Application Protocol
|
||||
Specification" document.
|
||||
|
||||
0x11 7 Return Slave Busy Count
|
||||
|
||||
Quantity of messages addressed to the remote device for which it
|
||||
returned a Slave Device Busy exception response, since its last
|
||||
restart, clear counters operation, or power-up. Exception
|
||||
responses are described and listed in "MODBUS Application
|
||||
Protocol Specification" document.
|
||||
|
||||
0x12 8 Return Bus Character Overrun Count
|
||||
|
||||
Quantity of messages addressed to the remote device that it could
|
||||
not handle due to a character overrun condition, since its last
|
||||
restart, clear counters operation, or power-up. A character
|
||||
overrun is caused by data characters arriving at the port faster
|
||||
than they can.
|
||||
|
||||
.. note:: I threw the event counter in here for convenience
|
||||
"""
|
||||
|
||||
__data = {i: 0x0000 for i in range(9)}
|
||||
__names = [
|
||||
"BusMessage",
|
||||
"BusCommunicationError",
|
||||
"SlaveExceptionError",
|
||||
"SlaveMessage",
|
||||
"SlaveNoResponse",
|
||||
"SlaveNAK",
|
||||
"SlaveBusy",
|
||||
"BusCharacterOverrun",
|
||||
]
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over the device counters.
|
||||
|
||||
:returns: An iterator of the device counters
|
||||
"""
|
||||
return zip(self.__names, iter(self.__data.values()))
|
||||
|
||||
def update(self, values):
|
||||
"""Update the values of this identity.
|
||||
|
||||
using another identify as the value
|
||||
|
||||
:param values: The value to copy values from
|
||||
"""
|
||||
for k, v_item in iter(values.items()):
|
||||
v_item += self.__getattribute__( # pylint: disable=unnecessary-dunder-call
|
||||
k
|
||||
)
|
||||
self.__setattr__(k, v_item) # pylint: disable=unnecessary-dunder-call
|
||||
|
||||
def reset(self):
|
||||
"""Clear all of the system counters."""
|
||||
self.__data = {i: 0x0000 for i in range(9)}
|
||||
|
||||
def summary(self):
|
||||
"""Return a summary of the counters current status.
|
||||
|
||||
:returns: A byte with each bit representing each counter
|
||||
"""
|
||||
count, result = 0x01, 0x00
|
||||
for i in iter(self.__data.values()):
|
||||
if i != 0x00: # pylint: disable=compare-to-zero
|
||||
result |= count
|
||||
count <<= 1
|
||||
return result
|
||||
|
||||
# -------------------------------------------------------------------------#
|
||||
# Properties
|
||||
# -------------------------------------------------------------------------#
|
||||
BusMessage = dict_property(
|
||||
lambda s: s.__data, 0 # pylint: disable=protected-access
|
||||
)
|
||||
BusCommunicationError = dict_property(
|
||||
lambda s: s.__data, 1 # pylint: disable=protected-access
|
||||
)
|
||||
BusExceptionError = dict_property(
|
||||
lambda s: s.__data, 2 # pylint: disable=protected-access
|
||||
)
|
||||
SlaveMessage = dict_property(
|
||||
lambda s: s.__data, 3 # pylint: disable=protected-access
|
||||
)
|
||||
SlaveNoResponse = dict_property(
|
||||
lambda s: s.__data, 4 # pylint: disable=protected-access
|
||||
)
|
||||
SlaveNAK = dict_property(lambda s: s.__data, 5) # pylint: disable=protected-access
|
||||
SlaveBusy = dict_property(lambda s: s.__data, 6) # pylint: disable=protected-access
|
||||
BusCharacterOverrun = dict_property(
|
||||
lambda s: s.__data, 7 # pylint: disable=protected-access
|
||||
)
|
||||
Event = dict_property(lambda s: s.__data, 8) # pylint: disable=protected-access
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Main server control block
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ModbusControlBlock:
|
||||
"""This is a global singleton that controls all system information.
|
||||
|
||||
All activity should be logged here and all diagnostic requests
|
||||
should come from here.
|
||||
"""
|
||||
|
||||
__mode = "ASCII"
|
||||
__diagnostic = [False] * 16
|
||||
__listen_only = False
|
||||
__delimiter = b"\r"
|
||||
__counters = ModbusCountersHandler()
|
||||
__identity = ModbusDeviceIdentification()
|
||||
__plus = ModbusPlusStatistics()
|
||||
__events: List[ModbusEvent] = []
|
||||
|
||||
# -------------------------------------------------------------------------#
|
||||
# Magic
|
||||
# -------------------------------------------------------------------------#
|
||||
def __str__(self):
|
||||
"""Build a representation of the control block.
|
||||
|
||||
:returns: A string representation of the control block
|
||||
"""
|
||||
return "ModbusControl"
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over the device counters.
|
||||
|
||||
:returns: An iterator of the device counters
|
||||
"""
|
||||
return self.__counters.__iter__()
|
||||
|
||||
def __new__(cls, *_args, **_kwargs):
|
||||
"""Create a new instance."""
|
||||
if "_inst" not in vars(cls):
|
||||
cls._inst = object.__new__(cls)
|
||||
return cls._inst
|
||||
|
||||
# -------------------------------------------------------------------------#
|
||||
# Events
|
||||
# -------------------------------------------------------------------------#
|
||||
def addEvent(self, event: ModbusEvent):
|
||||
"""Add a new event to the event log.
|
||||
|
||||
:param event: A new event to add to the log
|
||||
"""
|
||||
self.__events.insert(0, event)
|
||||
self.__events = self.__events[0:64] # chomp to 64 entries
|
||||
self.Counter.Event += 1
|
||||
|
||||
def getEvents(self):
|
||||
"""Return an encoded collection of the event log.
|
||||
|
||||
:returns: The encoded events packet
|
||||
"""
|
||||
events = [event.encode() for event in self.__events]
|
||||
return b"".join(events)
|
||||
|
||||
def clearEvents(self):
|
||||
"""Clear the current list of events."""
|
||||
self.__events = []
|
||||
|
||||
# -------------------------------------------------------------------------#
|
||||
# Other Properties
|
||||
# -------------------------------------------------------------------------#
|
||||
Identity = property(lambda s: s.__identity)
|
||||
Counter = property(lambda s: s.__counters)
|
||||
Events = property(lambda s: s.__events)
|
||||
Plus = property(lambda s: s.__plus)
|
||||
|
||||
def reset(self):
|
||||
"""Clear all of the system counters and the diagnostic register."""
|
||||
self.__events = []
|
||||
self.__counters.reset()
|
||||
self.__diagnostic = [False] * 16
|
||||
|
||||
# -------------------------------------------------------------------------#
|
||||
# Listen Properties
|
||||
# -------------------------------------------------------------------------#
|
||||
def _setListenOnly(self, value):
|
||||
"""Toggle the listen only status.
|
||||
|
||||
:param value: The value to set the listen status to
|
||||
"""
|
||||
self.__listen_only = bool(value) # pylint: disable=unused-private-member
|
||||
|
||||
ListenOnly = property(lambda s: s.__listen_only, _setListenOnly)
|
||||
|
||||
# -------------------------------------------------------------------------#
|
||||
# Mode Properties
|
||||
# -------------------------------------------------------------------------#
|
||||
def _setMode(self, mode):
|
||||
"""Toggle the current serial mode.
|
||||
|
||||
:param mode: The data transfer method in (RTU, ASCII)
|
||||
"""
|
||||
if mode in {"ASCII", "RTU"}:
|
||||
self.__mode = mode # pylint: disable=unused-private-member
|
||||
|
||||
Mode = property(lambda s: s.__mode, _setMode)
|
||||
|
||||
# -------------------------------------------------------------------------#
|
||||
# Delimiter Properties
|
||||
# -------------------------------------------------------------------------#
|
||||
def _setDelimiter(self, char):
|
||||
"""Change the serial delimiter character.
|
||||
|
||||
:param char: The new serial delimiter character
|
||||
"""
|
||||
if isinstance(char, str):
|
||||
self.__delimiter = char.encode() # pylint: disable=unused-private-member
|
||||
if isinstance(char, bytes):
|
||||
self.__delimiter = char # pylint: disable=unused-private-member
|
||||
elif isinstance(char, int):
|
||||
self.__delimiter = struct.pack( # pylint: disable=unused-private-member
|
||||
">B", char
|
||||
)
|
||||
|
||||
Delimiter = property(lambda s: s.__delimiter, _setDelimiter)
|
||||
|
||||
# -------------------------------------------------------------------------#
|
||||
# Diagnostic Properties
|
||||
# -------------------------------------------------------------------------#
|
||||
def setDiagnostic(self, mapping):
|
||||
"""Set the value in the diagnostic register.
|
||||
|
||||
:param mapping: Dictionary of key:value pairs to set
|
||||
"""
|
||||
for entry in iter(mapping.items()):
|
||||
if entry[0] >= 0 and entry[0] < len(self.__diagnostic):
|
||||
self.__diagnostic[entry[0]] = bool(entry[1])
|
||||
|
||||
def getDiagnostic(self, bit):
|
||||
"""Get the value in the diagnostic register.
|
||||
|
||||
:param bit: The bit to get
|
||||
:returns: The current value of the requested bit
|
||||
"""
|
||||
try:
|
||||
if bit and 0 <= bit < len(self.__diagnostic):
|
||||
return self.__diagnostic[bit]
|
||||
except Exception: # pylint: disable=broad-except
|
||||
return None
|
||||
return None
|
||||
|
||||
def getDiagnosticRegister(self):
|
||||
"""Get the entire diagnostic register.
|
||||
|
||||
:returns: The diagnostic register collection
|
||||
"""
|
||||
return self.__diagnostic
|
||||
870
env/lib/python3.11/site-packages/pymodbus/diag_message.py
vendored
Normal file
870
env/lib/python3.11/site-packages/pymodbus/diag_message.py
vendored
Normal file
@@ -0,0 +1,870 @@
|
||||
"""Diagnostic Record Read/Write.
|
||||
|
||||
These need to be tied into a the current server context
|
||||
or linked to the appropriate data
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
"DiagnosticStatusRequest",
|
||||
"DiagnosticStatusResponse",
|
||||
"ReturnQueryDataRequest",
|
||||
"ReturnQueryDataResponse",
|
||||
"RestartCommunicationsOptionRequest",
|
||||
"RestartCommunicationsOptionResponse",
|
||||
"ReturnDiagnosticRegisterRequest",
|
||||
"ReturnDiagnosticRegisterResponse",
|
||||
"ChangeAsciiInputDelimiterRequest",
|
||||
"ChangeAsciiInputDelimiterResponse",
|
||||
"ForceListenOnlyModeRequest",
|
||||
"ForceListenOnlyModeResponse",
|
||||
"ClearCountersRequest",
|
||||
"ClearCountersResponse",
|
||||
"ReturnBusMessageCountRequest",
|
||||
"ReturnBusMessageCountResponse",
|
||||
"ReturnBusCommunicationErrorCountRequest",
|
||||
"ReturnBusCommunicationErrorCountResponse",
|
||||
"ReturnBusExceptionErrorCountRequest",
|
||||
"ReturnBusExceptionErrorCountResponse",
|
||||
"ReturnSlaveMessageCountRequest",
|
||||
"ReturnSlaveMessageCountResponse",
|
||||
"ReturnSlaveNoResponseCountRequest",
|
||||
"ReturnSlaveNoResponseCountResponse",
|
||||
"ReturnSlaveNAKCountRequest",
|
||||
"ReturnSlaveNAKCountResponse",
|
||||
"ReturnSlaveBusyCountRequest",
|
||||
"ReturnSlaveBusyCountResponse",
|
||||
"ReturnSlaveBusCharacterOverrunCountRequest",
|
||||
"ReturnSlaveBusCharacterOverrunCountResponse",
|
||||
"ReturnIopOverrunCountRequest",
|
||||
"ReturnIopOverrunCountResponse",
|
||||
"ClearOverrunCountRequest",
|
||||
"ClearOverrunCountResponse",
|
||||
"GetClearModbusPlusRequest",
|
||||
"GetClearModbusPlusResponse",
|
||||
]
|
||||
|
||||
# pylint: disable=missing-type-doc
|
||||
import struct
|
||||
|
||||
from pymodbus.constants import ModbusPlusOperation, ModbusStatus
|
||||
from pymodbus.device import ModbusControlBlock
|
||||
from pymodbus.exceptions import ModbusException, NotImplementedException
|
||||
from pymodbus.pdu import ModbusRequest, ModbusResponse
|
||||
from pymodbus.utilities import pack_bitstring
|
||||
|
||||
|
||||
_MCB = ModbusControlBlock()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Diagnostic Function Codes Base Classes
|
||||
# diagnostic 08, 00-18,20
|
||||
# ---------------------------------------------------------------------------#
|
||||
# TODO Make sure all the data is decoded from the response # pylint: disable=fixme
|
||||
# ---------------------------------------------------------------------------#
|
||||
class DiagnosticStatusRequest(ModbusRequest):
|
||||
"""This is a base class for all of the diagnostic request functions."""
|
||||
|
||||
function_code = 0x08
|
||||
function_code_name = "diagnostic_status"
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize a diagnostic request."""
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.message = None
|
||||
|
||||
def encode(self):
|
||||
"""Encode a diagnostic response.
|
||||
|
||||
we encode the data set in self.message
|
||||
|
||||
:returns: The encoded packet
|
||||
"""
|
||||
packet = struct.pack(">H", self.sub_function_code)
|
||||
if self.message is not None:
|
||||
if isinstance(self.message, str):
|
||||
packet += self.message.encode()
|
||||
elif isinstance(self.message, bytes):
|
||||
packet += self.message
|
||||
elif isinstance(self.message, (list, tuple)):
|
||||
for piece in self.message:
|
||||
packet += struct.pack(">H", piece)
|
||||
elif isinstance(self.message, int):
|
||||
packet += struct.pack(">H", self.message)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode a diagnostic request.
|
||||
|
||||
:param data: The data to decode into the function code
|
||||
"""
|
||||
(
|
||||
self.sub_function_code, # pylint: disable=attribute-defined-outside-init
|
||||
) = struct.unpack(">H", data[:2])
|
||||
if self.sub_function_code == ReturnQueryDataRequest.sub_function_code:
|
||||
self.message = data[2:]
|
||||
else:
|
||||
(self.message,) = struct.unpack(">H", data[2:])
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""Get response pdu size.
|
||||
|
||||
Func_code (1 byte) + Sub function code (2 byte) + Data (2 * N bytes)
|
||||
:return:
|
||||
"""
|
||||
if not isinstance(self.message, list):
|
||||
self.message = [self.message]
|
||||
return 1 + 2 + 2 * len(self.message)
|
||||
|
||||
|
||||
class DiagnosticStatusResponse(ModbusResponse):
|
||||
"""Diagnostic status.
|
||||
|
||||
This is a base class for all of the diagnostic response functions
|
||||
|
||||
It works by performing all of the encoding and decoding of variable
|
||||
data and lets the higher classes define what extra data to append
|
||||
and how to execute a request
|
||||
"""
|
||||
|
||||
function_code = 0x08
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize a diagnostic response."""
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.message = None
|
||||
|
||||
def encode(self):
|
||||
"""Encode diagnostic response.
|
||||
|
||||
we encode the data set in self.message
|
||||
|
||||
:returns: The encoded packet
|
||||
"""
|
||||
packet = struct.pack(">H", self.sub_function_code)
|
||||
if self.message is not None:
|
||||
if isinstance(self.message, str):
|
||||
packet += self.message.encode()
|
||||
elif isinstance(self.message, bytes):
|
||||
packet += self.message
|
||||
elif isinstance(self.message, (list, tuple)):
|
||||
for piece in self.message:
|
||||
packet += struct.pack(">H", piece)
|
||||
elif isinstance(self.message, int):
|
||||
packet += struct.pack(">H", self.message)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode diagnostic response.
|
||||
|
||||
:param data: The data to decode into the function code
|
||||
"""
|
||||
(
|
||||
self.sub_function_code, # pylint: disable=attribute-defined-outside-init
|
||||
) = struct.unpack(">H", data[:2])
|
||||
data = data[2:]
|
||||
if self.sub_function_code == ReturnQueryDataRequest.sub_function_code:
|
||||
self.message = data
|
||||
else:
|
||||
word_len = len(data) // 2
|
||||
if len(data) % 2:
|
||||
word_len += 1
|
||||
data += b"0"
|
||||
data = struct.unpack(">" + "H" * word_len, data)
|
||||
self.message = data
|
||||
|
||||
|
||||
class DiagnosticStatusSimpleRequest(DiagnosticStatusRequest):
|
||||
"""Return diagnostic status.
|
||||
|
||||
A large majority of the diagnostic functions are simple
|
||||
status request functions. They work by sending 0x0000
|
||||
as data and their function code and they are returned
|
||||
2 bytes of data.
|
||||
|
||||
If a function inherits this, they only need to implement
|
||||
the execute method
|
||||
"""
|
||||
|
||||
def __init__(self, data=0x0000, **kwargs):
|
||||
"""Initialize a simple diagnostic request
|
||||
|
||||
The data defaults to 0x0000 if not provided as over half
|
||||
of the functions require it.
|
||||
|
||||
:param data: The data to send along with the request
|
||||
"""
|
||||
DiagnosticStatusRequest.__init__(self, **kwargs)
|
||||
self.message = data
|
||||
|
||||
def execute(self, *args):
|
||||
"""Raise if not implemented."""
|
||||
raise NotImplementedException("Diagnostic Message Has No Execute Method")
|
||||
|
||||
|
||||
class DiagnosticStatusSimpleResponse(DiagnosticStatusResponse):
|
||||
"""Diagnostic status.
|
||||
|
||||
A large majority of the diagnostic functions are simple
|
||||
status request functions. They work by sending 0x0000
|
||||
as data and their function code and they are returned
|
||||
2 bytes of data.
|
||||
"""
|
||||
|
||||
def __init__(self, data=0x0000, **kwargs):
|
||||
"""Return a simple diagnostic response.
|
||||
|
||||
:param data: The resulting data to return to the client
|
||||
"""
|
||||
DiagnosticStatusResponse.__init__(self, **kwargs)
|
||||
self.message = data
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 00
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ReturnQueryDataRequest(DiagnosticStatusRequest):
|
||||
"""Return query data.
|
||||
|
||||
The data passed in the request data field is to be returned (looped back)
|
||||
in the response. The entire response message should be identical to the
|
||||
request.
|
||||
"""
|
||||
|
||||
sub_function_code = 0x0000
|
||||
|
||||
def __init__(self, message=b"\x00\x00", slave=None, **kwargs):
|
||||
"""Initialize a new instance of the request.
|
||||
|
||||
:param message: The message to send to loopback
|
||||
"""
|
||||
DiagnosticStatusRequest.__init__(self, slave=slave, **kwargs)
|
||||
if not isinstance(message, bytes):
|
||||
raise ModbusException(f"message({type(message)}) must be bytes")
|
||||
self.message = message
|
||||
|
||||
def execute(self, *_args):
|
||||
"""Execute the loopback request (builds the response).
|
||||
|
||||
:returns: The populated loopback response message
|
||||
"""
|
||||
return ReturnQueryDataResponse(self.message)
|
||||
|
||||
|
||||
class ReturnQueryDataResponse(DiagnosticStatusResponse):
|
||||
"""Return query data.
|
||||
|
||||
The data passed in the request data field is to be returned (looped back)
|
||||
in the response. The entire response message should be identical to the
|
||||
request.
|
||||
"""
|
||||
|
||||
sub_function_code = 0x0000
|
||||
|
||||
def __init__(self, message=b"\x00\x00", **kwargs):
|
||||
"""Initialize a new instance of the response.
|
||||
|
||||
:param message: The message to loopback
|
||||
"""
|
||||
DiagnosticStatusResponse.__init__(self, **kwargs)
|
||||
if not isinstance(message, bytes):
|
||||
raise ModbusException(f"message({type(message)}) must be bytes")
|
||||
self.message = message
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 01
|
||||
# ---------------------------------------------------------------------------#
|
||||
class RestartCommunicationsOptionRequest(DiagnosticStatusRequest):
|
||||
"""Restart communication.
|
||||
|
||||
The remote device serial line port must be initialized and restarted, and
|
||||
all of its communications event counters are cleared. If the port is
|
||||
currently in Listen Only Mode, no response is returned. This function is
|
||||
the only one that brings the port out of Listen Only Mode. If the port is
|
||||
not currently in Listen Only Mode, a normal response is returned. This
|
||||
occurs before the restart is executed.
|
||||
"""
|
||||
|
||||
sub_function_code = 0x0001
|
||||
|
||||
def __init__(self, toggle=False, slave=None, **kwargs):
|
||||
"""Initialize a new request.
|
||||
|
||||
:param toggle: Set to True to toggle, False otherwise
|
||||
"""
|
||||
DiagnosticStatusRequest.__init__(self, slave=slave, **kwargs)
|
||||
if toggle:
|
||||
self.message = [ModbusStatus.ON]
|
||||
else:
|
||||
self.message = [ModbusStatus.OFF]
|
||||
|
||||
def execute(self, *_args):
|
||||
"""Clear event log and restart.
|
||||
|
||||
:returns: The initialized response message
|
||||
"""
|
||||
# if _MCB.ListenOnly:
|
||||
return RestartCommunicationsOptionResponse(self.message)
|
||||
|
||||
|
||||
class RestartCommunicationsOptionResponse(DiagnosticStatusResponse):
|
||||
"""Restart Communication.
|
||||
|
||||
The remote device serial line port must be initialized and restarted, and
|
||||
all of its communications event counters are cleared. If the port is
|
||||
currently in Listen Only Mode, no response is returned. This function is
|
||||
the only one that brings the port out of Listen Only Mode. If the port is
|
||||
not currently in Listen Only Mode, a normal response is returned. This
|
||||
occurs before the restart is executed.
|
||||
"""
|
||||
|
||||
sub_function_code = 0x0001
|
||||
|
||||
def __init__(self, toggle=False, **kwargs):
|
||||
"""Initialize a new response.
|
||||
|
||||
:param toggle: Set to True if we toggled, False otherwise
|
||||
"""
|
||||
DiagnosticStatusResponse.__init__(self, **kwargs)
|
||||
if toggle:
|
||||
self.message = [ModbusStatus.ON]
|
||||
else:
|
||||
self.message = [ModbusStatus.OFF]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 02
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ReturnDiagnosticRegisterRequest(DiagnosticStatusSimpleRequest):
|
||||
"""The contents of the remote device's 16-bit diagnostic register are returned in the response."""
|
||||
|
||||
sub_function_code = 0x0002
|
||||
|
||||
def execute(self, *args):
|
||||
"""Execute the diagnostic request on the given device.
|
||||
|
||||
:returns: The initialized response message
|
||||
"""
|
||||
# if _MCB.isListenOnly():
|
||||
register = pack_bitstring(_MCB.getDiagnosticRegister())
|
||||
return ReturnDiagnosticRegisterResponse(register)
|
||||
|
||||
|
||||
class ReturnDiagnosticRegisterResponse(DiagnosticStatusSimpleResponse):
|
||||
"""Return diagnostic register.
|
||||
|
||||
The contents of the remote device's 16-bit diagnostic register are
|
||||
returned in the response
|
||||
"""
|
||||
|
||||
sub_function_code = 0x0002
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 03
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ChangeAsciiInputDelimiterRequest(DiagnosticStatusSimpleRequest):
|
||||
"""Change ascii input delimiter.
|
||||
|
||||
The character "CHAR" passed in the request data field becomes the end of
|
||||
message delimiter for future messages (replacing the default LF
|
||||
character). This function is useful in cases of a Line Feed is not
|
||||
required at the end of ASCII messages.
|
||||
"""
|
||||
|
||||
sub_function_code = 0x0003
|
||||
|
||||
def execute(self, *args):
|
||||
"""Execute the diagnostic request on the given device.
|
||||
|
||||
:returns: The initialized response message
|
||||
"""
|
||||
char = (self.message & 0xFF00) >> 8
|
||||
_MCB._setDelimiter(char) # pylint: disable=protected-access
|
||||
return ChangeAsciiInputDelimiterResponse(self.message)
|
||||
|
||||
|
||||
class ChangeAsciiInputDelimiterResponse(DiagnosticStatusSimpleResponse):
|
||||
"""Change ascii input delimiter.
|
||||
|
||||
The character "CHAR" passed in the request data field becomes the end of
|
||||
message delimiter for future messages (replacing the default LF
|
||||
character). This function is useful in cases of a Line Feed is not
|
||||
required at the end of ASCII messages.
|
||||
"""
|
||||
|
||||
sub_function_code = 0x0003
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 04
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ForceListenOnlyModeRequest(DiagnosticStatusSimpleRequest):
|
||||
"""Forces the addressed remote device to its Listen Only Mode for MODBUS communications.
|
||||
|
||||
This isolates it from the other devices on the network,
|
||||
allowing them to continue communicating without interruption from the
|
||||
addressed remote device. No response is returned.
|
||||
"""
|
||||
|
||||
sub_function_code = 0x0004
|
||||
|
||||
def execute(self, *args):
|
||||
"""Execute the diagnostic request on the given device.
|
||||
|
||||
:returns: The initialized response message
|
||||
"""
|
||||
_MCB._setListenOnly(True) # pylint: disable=protected-access
|
||||
return ForceListenOnlyModeResponse()
|
||||
|
||||
|
||||
class ForceListenOnlyModeResponse(DiagnosticStatusResponse):
|
||||
"""Forces the addressed remote device to its Listen Only Mode for MODBUS communications.
|
||||
|
||||
This isolates it from the other devices on the network,
|
||||
allowing them to continue communicating without interruption from the
|
||||
addressed remote device. No response is returned.
|
||||
|
||||
This does not send a response
|
||||
"""
|
||||
|
||||
sub_function_code = 0x0004
|
||||
should_respond = False
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize to block a return response."""
|
||||
DiagnosticStatusResponse.__init__(self, **kwargs)
|
||||
self.message = []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 10
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ClearCountersRequest(DiagnosticStatusSimpleRequest):
|
||||
"""Clear ll counters and the diagnostic register.
|
||||
|
||||
Also, counters are cleared upon power-up
|
||||
"""
|
||||
|
||||
sub_function_code = 0x000A
|
||||
|
||||
def execute(self, *args):
|
||||
"""Execute the diagnostic request on the given device.
|
||||
|
||||
:returns: The initialized response message
|
||||
"""
|
||||
_MCB.reset()
|
||||
return ClearCountersResponse(self.message)
|
||||
|
||||
|
||||
class ClearCountersResponse(DiagnosticStatusSimpleResponse):
|
||||
"""Clear ll counters and the diagnostic register.
|
||||
|
||||
Also, counters are cleared upon power-up
|
||||
"""
|
||||
|
||||
sub_function_code = 0x000A
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 11
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ReturnBusMessageCountRequest(DiagnosticStatusSimpleRequest):
|
||||
"""Return bus message count.
|
||||
|
||||
The response data field returns the quantity of messages that the
|
||||
remote device has detected on the communications systems since its last
|
||||
restart, clear counters operation, or power-up
|
||||
"""
|
||||
|
||||
sub_function_code = 0x000B
|
||||
|
||||
def execute(self, *args):
|
||||
"""Execute the diagnostic request on the given device.
|
||||
|
||||
:returns: The initialized response message
|
||||
"""
|
||||
count = _MCB.Counter.BusMessage
|
||||
return ReturnBusMessageCountResponse(count)
|
||||
|
||||
|
||||
class ReturnBusMessageCountResponse(DiagnosticStatusSimpleResponse):
|
||||
"""Return bus message count.
|
||||
|
||||
The response data field returns the quantity of messages that the
|
||||
remote device has detected on the communications systems since its last
|
||||
restart, clear counters operation, or power-up
|
||||
"""
|
||||
|
||||
sub_function_code = 0x000B
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 12
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ReturnBusCommunicationErrorCountRequest(DiagnosticStatusSimpleRequest):
|
||||
"""Return bus comm. count.
|
||||
|
||||
The response data field returns the quantity of CRC errors encountered
|
||||
by the remote device since its last restart, clear counter operation, or
|
||||
power-up
|
||||
"""
|
||||
|
||||
sub_function_code = 0x000C
|
||||
|
||||
def execute(self, *args):
|
||||
"""Execute the diagnostic request on the given device.
|
||||
|
||||
:returns: The initialized response message
|
||||
"""
|
||||
count = _MCB.Counter.BusCommunicationError
|
||||
return ReturnBusCommunicationErrorCountResponse(count)
|
||||
|
||||
|
||||
class ReturnBusCommunicationErrorCountResponse(DiagnosticStatusSimpleResponse):
|
||||
"""Return bus comm. error.
|
||||
|
||||
The response data field returns the quantity of CRC errors encountered
|
||||
by the remote device since its last restart, clear counter operation, or
|
||||
power-up
|
||||
"""
|
||||
|
||||
sub_function_code = 0x000C
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 13
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ReturnBusExceptionErrorCountRequest(DiagnosticStatusSimpleRequest):
|
||||
"""Return bus exception.
|
||||
|
||||
The response data field returns the quantity of modbus exception
|
||||
responses returned by the remote device since its last restart,
|
||||
clear counters operation, or power-up
|
||||
"""
|
||||
|
||||
sub_function_code = 0x000D
|
||||
|
||||
def execute(self, *args):
|
||||
"""Execute the diagnostic request on the given device.
|
||||
|
||||
:returns: The initialized response message
|
||||
"""
|
||||
count = _MCB.Counter.BusExceptionError
|
||||
return ReturnBusExceptionErrorCountResponse(count)
|
||||
|
||||
|
||||
class ReturnBusExceptionErrorCountResponse(DiagnosticStatusSimpleResponse):
|
||||
"""Return bus exception.
|
||||
|
||||
The response data field returns the quantity of modbus exception
|
||||
responses returned by the remote device since its last restart,
|
||||
clear counters operation, or power-up
|
||||
"""
|
||||
|
||||
sub_function_code = 0x000D
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 14
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ReturnSlaveMessageCountRequest(DiagnosticStatusSimpleRequest):
|
||||
"""Return slave message count.
|
||||
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device, or broadcast, that the remote device has processed since
|
||||
its last restart, clear counters operation, or power-up
|
||||
"""
|
||||
|
||||
sub_function_code = 0x000E
|
||||
|
||||
def execute(self, *args):
|
||||
"""Execute the diagnostic request on the given device.
|
||||
|
||||
:returns: The initialized response message
|
||||
"""
|
||||
count = _MCB.Counter.SlaveMessage
|
||||
return ReturnSlaveMessageCountResponse(count)
|
||||
|
||||
|
||||
class ReturnSlaveMessageCountResponse(DiagnosticStatusSimpleResponse):
|
||||
"""Return slave message count.
|
||||
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device, or broadcast, that the remote device has processed since
|
||||
its last restart, clear counters operation, or power-up
|
||||
"""
|
||||
|
||||
sub_function_code = 0x000E
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 15
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ReturnSlaveNoResponseCountRequest(DiagnosticStatusSimpleRequest):
|
||||
"""Return slave no response.
|
||||
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device, or broadcast, that the remote device has processed since
|
||||
its last restart, clear counters operation, or power-up
|
||||
"""
|
||||
|
||||
sub_function_code = 0x000F
|
||||
|
||||
def execute(self, *args):
|
||||
"""Execute the diagnostic request on the given device.
|
||||
|
||||
:returns: The initialized response message
|
||||
"""
|
||||
count = _MCB.Counter.SlaveNoResponse
|
||||
return ReturnSlaveNoResponseCountResponse(count)
|
||||
|
||||
|
||||
class ReturnSlaveNoResponseCountResponse(DiagnosticStatusSimpleResponse):
|
||||
"""Return slave no response.
|
||||
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device, or broadcast, that the remote device has processed since
|
||||
its last restart, clear counters operation, or power-up
|
||||
"""
|
||||
|
||||
sub_function_code = 0x000F
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 16
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ReturnSlaveNAKCountRequest(DiagnosticStatusSimpleRequest):
|
||||
"""Return slave NAK count.
|
||||
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device for which it returned a Negative Acknowledge (NAK) exception
|
||||
response, since its last restart, clear counters operation, or power-up.
|
||||
Exception responses are described and listed in section 7 .
|
||||
"""
|
||||
|
||||
sub_function_code = 0x0010
|
||||
|
||||
def execute(self, *args):
|
||||
"""Execute the diagnostic request on the given device.
|
||||
|
||||
:returns: The initialized response message
|
||||
"""
|
||||
count = _MCB.Counter.SlaveNAK
|
||||
return ReturnSlaveNAKCountResponse(count)
|
||||
|
||||
|
||||
class ReturnSlaveNAKCountResponse(DiagnosticStatusSimpleResponse):
|
||||
"""Return slave NAK.
|
||||
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device for which it returned a Negative Acknowledge (NAK) exception
|
||||
response, since its last restart, clear counters operation, or power-up.
|
||||
Exception responses are described and listed in section 7.
|
||||
"""
|
||||
|
||||
sub_function_code = 0x0010
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 17
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ReturnSlaveBusyCountRequest(DiagnosticStatusSimpleRequest):
|
||||
"""Return slave busy count.
|
||||
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device for which it returned a Slave Device Busy exception response,
|
||||
since its last restart, clear counters operation, or power-up.
|
||||
"""
|
||||
|
||||
sub_function_code = 0x0011
|
||||
|
||||
def execute(self, *args):
|
||||
"""Execute the diagnostic request on the given device.
|
||||
|
||||
:returns: The initialized response message
|
||||
"""
|
||||
count = _MCB.Counter.SlaveBusy
|
||||
return ReturnSlaveBusyCountResponse(count)
|
||||
|
||||
|
||||
class ReturnSlaveBusyCountResponse(DiagnosticStatusSimpleResponse):
|
||||
"""Return slave busy count.
|
||||
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device for which it returned a Slave Device Busy exception response,
|
||||
since its last restart, clear counters operation, or power-up.
|
||||
"""
|
||||
|
||||
sub_function_code = 0x0011
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 18
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ReturnSlaveBusCharacterOverrunCountRequest(DiagnosticStatusSimpleRequest):
|
||||
"""Return slave character overrun.
|
||||
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device that it could not handle due to a character overrun condition,
|
||||
since its last restart, clear counters operation, or power-up. A character
|
||||
overrun is caused by data characters arriving at the port faster than they
|
||||
can be stored, or by the loss of a character due to a hardware malfunction.
|
||||
"""
|
||||
|
||||
sub_function_code = 0x0012
|
||||
|
||||
def execute(self, *args):
|
||||
"""Execute the diagnostic request on the given device.
|
||||
|
||||
:returns: The initialized response message
|
||||
"""
|
||||
count = _MCB.Counter.BusCharacterOverrun
|
||||
return ReturnSlaveBusCharacterOverrunCountResponse(count)
|
||||
|
||||
|
||||
class ReturnSlaveBusCharacterOverrunCountResponse(DiagnosticStatusSimpleResponse):
|
||||
"""Return the quantity of messages addressed to the remote device unhandled due to a character overrun.
|
||||
|
||||
Since its last restart, clear counters operation, or power-up. A character
|
||||
overrun is caused by data characters arriving at the port faster than they
|
||||
can be stored, or by the loss of a character due to a hardware malfunction.
|
||||
"""
|
||||
|
||||
sub_function_code = 0x0012
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 19
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ReturnIopOverrunCountRequest(DiagnosticStatusSimpleRequest):
|
||||
"""Return IopOverrun.
|
||||
|
||||
An IOP overrun is caused by data characters arriving at the port
|
||||
faster than they can be stored, or by the loss of a character due
|
||||
to a hardware malfunction. This function is specific to the 884.
|
||||
"""
|
||||
|
||||
sub_function_code = 0x0013
|
||||
|
||||
def execute(self, *args):
|
||||
"""Execute the diagnostic request on the given device.
|
||||
|
||||
:returns: The initialized response message
|
||||
"""
|
||||
count = _MCB.Counter.BusCharacterOverrun
|
||||
return ReturnIopOverrunCountResponse(count)
|
||||
|
||||
|
||||
class ReturnIopOverrunCountResponse(DiagnosticStatusSimpleResponse):
|
||||
"""Return Iop overrun count.
|
||||
|
||||
The response data field returns the quantity of messages
|
||||
addressed to the slave that it could not handle due to an 884
|
||||
IOP overrun condition, since its last restart, clear counters
|
||||
operation, or power-up.
|
||||
"""
|
||||
|
||||
sub_function_code = 0x0013
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 20
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ClearOverrunCountRequest(DiagnosticStatusSimpleRequest):
|
||||
"""Clear the overrun error counter and reset the error flag.
|
||||
|
||||
An error flag should be cleared, but nothing else in the
|
||||
specification mentions is, so it is ignored.
|
||||
"""
|
||||
|
||||
sub_function_code = 0x0014
|
||||
|
||||
def execute(self, *args):
|
||||
"""Execute the diagnostic request on the given device.
|
||||
|
||||
:returns: The initialized response message
|
||||
"""
|
||||
_MCB.Counter.BusCharacterOverrun = 0x0000
|
||||
return ClearOverrunCountResponse(self.message)
|
||||
|
||||
|
||||
class ClearOverrunCountResponse(DiagnosticStatusSimpleResponse):
|
||||
"""Clear the overrun error counter and reset the error flag."""
|
||||
|
||||
sub_function_code = 0x0014
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 21
|
||||
# ---------------------------------------------------------------------------#
|
||||
class GetClearModbusPlusRequest(DiagnosticStatusSimpleRequest):
|
||||
"""Get/Clear modbus plus request.
|
||||
|
||||
In addition to the Function code (08) and Subfunction code
|
||||
(00 15 hex) in the query, a two-byte Operation field is used
|
||||
to specify either a "Get Statistics" or a "Clear Statistics"
|
||||
operation. The two operations are exclusive - the "Get"
|
||||
operation cannot clear the statistics, and the "Clear"
|
||||
operation does not return statistics prior to clearing
|
||||
them. Statistics are also cleared on power-up of the slave
|
||||
device.
|
||||
"""
|
||||
|
||||
sub_function_code = 0x0015
|
||||
|
||||
def __init__(self, slave=None, **kwargs):
|
||||
"""Initialize."""
|
||||
super().__init__(slave=slave, **kwargs)
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""Return a series of 54 16-bit words (108 bytes) in the data field of the response.
|
||||
|
||||
This function differs from the usual two-byte length of the data field.
|
||||
The data contains the statistics for the Modbus Plus peer processor in the slave device.
|
||||
Func_code (1 byte) + Sub function code (2 byte) + Operation (2 byte) + Data (108 bytes)
|
||||
:return:
|
||||
"""
|
||||
if self.message == ModbusPlusOperation.GET_STATISTICS:
|
||||
data = 2 + 108 # byte count(2) + data (54*2)
|
||||
else:
|
||||
data = 0
|
||||
return 1 + 2 + 2 + 2 + data
|
||||
|
||||
def execute(self, *args):
|
||||
"""Execute the diagnostic request on the given device.
|
||||
|
||||
:returns: The initialized response message
|
||||
"""
|
||||
message = None # the clear operation does not return info
|
||||
if self.message == ModbusPlusOperation.CLEAR_STATISTICS:
|
||||
_MCB.Plus.reset()
|
||||
message = self.message
|
||||
else:
|
||||
message = [self.message]
|
||||
message += _MCB.Plus.encode()
|
||||
return GetClearModbusPlusResponse(message)
|
||||
|
||||
def encode(self):
|
||||
"""Encode a diagnostic response.
|
||||
|
||||
we encode the data set in self.message
|
||||
|
||||
:returns: The encoded packet
|
||||
"""
|
||||
packet = struct.pack(">H", self.sub_function_code)
|
||||
packet += struct.pack(">H", self.message)
|
||||
return packet
|
||||
|
||||
|
||||
class GetClearModbusPlusResponse(DiagnosticStatusSimpleResponse):
|
||||
"""Return a series of 54 16-bit words (108 bytes) in the data field of the response.
|
||||
|
||||
This function differs from the usual two-byte length of the data field.
|
||||
The data contains the statistics for the Modbus Plus peer processor in the slave device.
|
||||
"""
|
||||
|
||||
sub_function_code = 0x0015
|
||||
203
env/lib/python3.11/site-packages/pymodbus/events.py
vendored
Normal file
203
env/lib/python3.11/site-packages/pymodbus/events.py
vendored
Normal file
@@ -0,0 +1,203 @@
|
||||
"""Modbus Remote Events.
|
||||
|
||||
An event byte returned by the Get Communications Event Log function
|
||||
can be any one of four types. The type is defined by bit 7
|
||||
(the high-order bit) in each byte. It may be further defined by bit 6.
|
||||
"""
|
||||
# pylint: disable=missing-type-doc
|
||||
from pymodbus.exceptions import NotImplementedException, ParameterException
|
||||
from pymodbus.utilities import pack_bitstring, unpack_bitstring
|
||||
|
||||
|
||||
class ModbusEvent:
|
||||
"""Define modbus events."""
|
||||
|
||||
def encode(self):
|
||||
"""Encode the status bits to an event message.
|
||||
|
||||
:raises NotImplementedException:
|
||||
"""
|
||||
raise NotImplementedException
|
||||
|
||||
def decode(self, event):
|
||||
"""Decode the event message to its status bits.
|
||||
|
||||
:param event: The event to decode
|
||||
:raises NotImplementedException:
|
||||
"""
|
||||
raise NotImplementedException
|
||||
|
||||
|
||||
class RemoteReceiveEvent(ModbusEvent):
|
||||
"""Remote device MODBUS Receive Event.
|
||||
|
||||
The remote device stores this type of event byte when a query message
|
||||
is received. It is stored before the remote device processes the message.
|
||||
This event is defined by bit 7 set to logic "1". The other bits will be
|
||||
set to a logic "1" if the corresponding condition is TRUE. The bit layout
|
||||
is::
|
||||
|
||||
Bit Contents
|
||||
----------------------------------
|
||||
0 Not Used
|
||||
2 Not Used
|
||||
3 Not Used
|
||||
4 Character Overrun
|
||||
5 Currently in Listen Only Mode
|
||||
6 Broadcast Receive
|
||||
7 1
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize a new event instance."""
|
||||
self.overrun = kwargs.get("overrun", False)
|
||||
self.listen = kwargs.get("listen", False)
|
||||
self.broadcast = kwargs.get("broadcast", False)
|
||||
|
||||
def encode(self) -> bytes:
|
||||
"""Encode the status bits to an event message.
|
||||
|
||||
:returns: The encoded event message
|
||||
"""
|
||||
bits = [False] * 3
|
||||
bits += [self.overrun, self.listen, self.broadcast, True]
|
||||
packet = pack_bitstring(bits)
|
||||
return packet
|
||||
|
||||
def decode(self, event: bytes) -> None:
|
||||
"""Decode the event message to its status bits.
|
||||
|
||||
:param event: The event to decode
|
||||
"""
|
||||
bits = unpack_bitstring(event)
|
||||
self.overrun = bits[4]
|
||||
self.listen = bits[5]
|
||||
self.broadcast = bits[6]
|
||||
|
||||
|
||||
class RemoteSendEvent(ModbusEvent):
|
||||
"""Remote device MODBUS Send Event.
|
||||
|
||||
The remote device stores this type of event byte when it finishes
|
||||
processing a request message. It is stored if the remote device
|
||||
returned a normal or exception response, or no response.
|
||||
|
||||
This event is defined by bit 7 set to a logic "0", with bit 6 set to a "1".
|
||||
The other bits will be set to a logic "1" if the corresponding
|
||||
condition is TRUE. The bit layout is::
|
||||
|
||||
Bit Contents
|
||||
-----------------------------------------------------------
|
||||
0 Read Exception Sent (Exception Codes 1-3)
|
||||
1 Slave Abort Exception Sent (Exception Code 4)
|
||||
2 Slave Busy Exception Sent (Exception Codes 5-6)
|
||||
3 Slave Program NAK Exception Sent (Exception Code 7)
|
||||
4 Write Timeout Error Occurred
|
||||
5 Currently in Listen Only Mode
|
||||
6 1
|
||||
7 0
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize a new event instance."""
|
||||
self.read = kwargs.get("read", False)
|
||||
self.slave_abort = kwargs.get("slave_abort", False)
|
||||
self.slave_busy = kwargs.get("slave_busy", False)
|
||||
self.slave_nak = kwargs.get("slave_nak", False)
|
||||
self.write_timeout = kwargs.get("write_timeout", False)
|
||||
self.listen = kwargs.get("listen", False)
|
||||
|
||||
def encode(self):
|
||||
"""Encode the status bits to an event message.
|
||||
|
||||
:returns: The encoded event message
|
||||
"""
|
||||
bits = [
|
||||
self.read,
|
||||
self.slave_abort,
|
||||
self.slave_busy,
|
||||
self.slave_nak,
|
||||
self.write_timeout,
|
||||
self.listen,
|
||||
]
|
||||
bits += [True, False]
|
||||
packet = pack_bitstring(bits)
|
||||
return packet
|
||||
|
||||
def decode(self, event):
|
||||
"""Decode the event message to its status bits.
|
||||
|
||||
:param event: The event to decode
|
||||
"""
|
||||
# todo fix the start byte count # pylint: disable=fixme
|
||||
bits = unpack_bitstring(event)
|
||||
self.read = bits[0]
|
||||
self.slave_abort = bits[1]
|
||||
self.slave_busy = bits[2]
|
||||
self.slave_nak = bits[3]
|
||||
self.write_timeout = bits[4]
|
||||
self.listen = bits[5]
|
||||
|
||||
|
||||
class EnteredListenModeEvent(ModbusEvent):
|
||||
"""Enter Remote device Listen Only Mode
|
||||
|
||||
The remote device stores this type of event byte when it enters
|
||||
the Listen Only Mode. The event is defined by a content of 04 hex.
|
||||
"""
|
||||
|
||||
value = 0x04
|
||||
__encoded = b"\x04"
|
||||
|
||||
def encode(self):
|
||||
"""Encode the status bits to an event message.
|
||||
|
||||
:returns: The encoded event message
|
||||
"""
|
||||
return self.__encoded
|
||||
|
||||
def decode(self, event):
|
||||
"""Decode the event message to its status bits.
|
||||
|
||||
:param event: The event to decode
|
||||
:raises ParameterException:
|
||||
"""
|
||||
if event != self.__encoded:
|
||||
raise ParameterException("Invalid decoded value")
|
||||
|
||||
|
||||
class CommunicationRestartEvent(ModbusEvent):
|
||||
"""Restart remote device Initiated Communication.
|
||||
|
||||
The remote device stores this type of event byte when its communications
|
||||
port is restarted. The remote device can be restarted by the Diagnostics
|
||||
function (code 08), with sub-function Restart Communications Option
|
||||
(code 00 01).
|
||||
|
||||
That function also places the remote device into a "Continue on Error"
|
||||
or "Stop on Error" mode. If the remote device is placed into "Continue on
|
||||
Error" mode, the event byte is added to the existing event log. If the
|
||||
remote device is placed into "Stop on Error" mode, the byte is added to
|
||||
the log and the rest of the log is cleared to zeros.
|
||||
|
||||
The event is defined by a content of zero.
|
||||
"""
|
||||
|
||||
value = 0x00
|
||||
__encoded = b"\x00"
|
||||
|
||||
def encode(self):
|
||||
"""Encode the status bits to an event message.
|
||||
|
||||
:returns: The encoded event message
|
||||
"""
|
||||
return self.__encoded
|
||||
|
||||
def decode(self, event):
|
||||
"""Decode the event message to its status bits.
|
||||
|
||||
:param event: The event to decode
|
||||
:raises ParameterException:
|
||||
"""
|
||||
if event != self.__encoded:
|
||||
raise ParameterException("Invalid decoded value")
|
||||
117
env/lib/python3.11/site-packages/pymodbus/exceptions.py
vendored
Normal file
117
env/lib/python3.11/site-packages/pymodbus/exceptions.py
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
"""Pymodbus Exceptions.
|
||||
|
||||
Custom exceptions to be used in the Modbus code.
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
"ModbusException",
|
||||
"ModbusIOException",
|
||||
"ParameterException",
|
||||
"NotImplementedException",
|
||||
"ConnectionException",
|
||||
"NoSuchSlaveException",
|
||||
"InvalidMessageReceivedException",
|
||||
"MessageRegisterException",
|
||||
]
|
||||
|
||||
|
||||
class ModbusException(Exception):
|
||||
"""Base modbus exception."""
|
||||
|
||||
def __init__(self, string):
|
||||
"""Initialize the exception.
|
||||
|
||||
:param string: The message to append to the error
|
||||
"""
|
||||
self.string = string
|
||||
super().__init__(string)
|
||||
|
||||
def __str__(self):
|
||||
"""Return string representation."""
|
||||
return f"Modbus Error: {self.string}"
|
||||
|
||||
def isError(self):
|
||||
"""Error"""
|
||||
return True
|
||||
|
||||
|
||||
class ModbusIOException(ModbusException):
|
||||
"""Error resulting from data i/o."""
|
||||
|
||||
def __init__(self, string="", function_code=None):
|
||||
"""Initialize the exception.
|
||||
|
||||
:param string: The message to append to the error
|
||||
"""
|
||||
self.fcode = function_code
|
||||
self.message = f"[Input/Output] {string}"
|
||||
ModbusException.__init__(self, self.message)
|
||||
|
||||
|
||||
class ParameterException(ModbusException):
|
||||
"""Error resulting from invalid parameter."""
|
||||
|
||||
def __init__(self, string=""):
|
||||
"""Initialize the exception.
|
||||
|
||||
:param string: The message to append to the error
|
||||
"""
|
||||
message = f"[Invalid Parameter] {string}"
|
||||
ModbusException.__init__(self, message)
|
||||
|
||||
|
||||
class NoSuchSlaveException(ModbusException):
|
||||
"""Error resulting from making a request to a slave that does not exist."""
|
||||
|
||||
def __init__(self, string=""):
|
||||
"""Initialize the exception.
|
||||
|
||||
:param string: The message to append to the error
|
||||
"""
|
||||
message = f"[No Such Slave] {string}"
|
||||
ModbusException.__init__(self, message)
|
||||
|
||||
|
||||
class NotImplementedException(ModbusException):
|
||||
"""Error resulting from not implemented function."""
|
||||
|
||||
def __init__(self, string=""):
|
||||
"""Initialize the exception.
|
||||
|
||||
:param string: The message to append to the error
|
||||
"""
|
||||
message = f"[Not Implemented] {string}"
|
||||
ModbusException.__init__(self, message)
|
||||
|
||||
|
||||
class ConnectionException(ModbusException):
|
||||
"""Error resulting from a bad connection."""
|
||||
|
||||
def __init__(self, string=""):
|
||||
"""Initialize the exception.
|
||||
|
||||
:param string: The message to append to the error
|
||||
"""
|
||||
message = f"[Connection] {string}"
|
||||
ModbusException.__init__(self, message)
|
||||
|
||||
|
||||
class InvalidMessageReceivedException(ModbusException):
|
||||
"""Error resulting from invalid response received or decoded."""
|
||||
|
||||
def __init__(self, string=""):
|
||||
"""Initialize the exception.
|
||||
|
||||
:param string: The message to append to the error
|
||||
"""
|
||||
message = f"[Invalid Message] {string}"
|
||||
ModbusException.__init__(self, message)
|
||||
|
||||
|
||||
class MessageRegisterException(ModbusException):
|
||||
"""Error resulting from failing to register a custom message request/response."""
|
||||
|
||||
def __init__(self, string=""):
|
||||
"""Initialize."""
|
||||
message = f"[Error registering message] {string}"
|
||||
ModbusException.__init__(self, message)
|
||||
374
env/lib/python3.11/site-packages/pymodbus/factory.py
vendored
Normal file
374
env/lib/python3.11/site-packages/pymodbus/factory.py
vendored
Normal file
@@ -0,0 +1,374 @@
|
||||
"""Modbus Request/Response Decoder Factories.
|
||||
|
||||
The following factories make it easy to decode request/response messages.
|
||||
To add a new request/response pair to be decodeable by the library, simply
|
||||
add them to the respective function lookup table (order doesn't matter, but
|
||||
it does help keep things organized).
|
||||
|
||||
Regardless of how many functions are added to the lookup, O(1) behavior is
|
||||
kept as a result of a pre-computed lookup dictionary.
|
||||
"""
|
||||
|
||||
# pylint: disable=missing-type-doc
|
||||
from pymodbus.bit_read_message import (
|
||||
ReadCoilsRequest,
|
||||
ReadCoilsResponse,
|
||||
ReadDiscreteInputsRequest,
|
||||
ReadDiscreteInputsResponse,
|
||||
)
|
||||
from pymodbus.bit_write_message import (
|
||||
WriteMultipleCoilsRequest,
|
||||
WriteMultipleCoilsResponse,
|
||||
WriteSingleCoilRequest,
|
||||
WriteSingleCoilResponse,
|
||||
)
|
||||
from pymodbus.diag_message import (
|
||||
ChangeAsciiInputDelimiterRequest,
|
||||
ChangeAsciiInputDelimiterResponse,
|
||||
ClearCountersRequest,
|
||||
ClearCountersResponse,
|
||||
ClearOverrunCountRequest,
|
||||
ClearOverrunCountResponse,
|
||||
DiagnosticStatusRequest,
|
||||
DiagnosticStatusResponse,
|
||||
ForceListenOnlyModeRequest,
|
||||
ForceListenOnlyModeResponse,
|
||||
GetClearModbusPlusRequest,
|
||||
GetClearModbusPlusResponse,
|
||||
RestartCommunicationsOptionRequest,
|
||||
RestartCommunicationsOptionResponse,
|
||||
ReturnBusCommunicationErrorCountRequest,
|
||||
ReturnBusCommunicationErrorCountResponse,
|
||||
ReturnBusExceptionErrorCountRequest,
|
||||
ReturnBusExceptionErrorCountResponse,
|
||||
ReturnBusMessageCountRequest,
|
||||
ReturnBusMessageCountResponse,
|
||||
ReturnDiagnosticRegisterRequest,
|
||||
ReturnDiagnosticRegisterResponse,
|
||||
ReturnIopOverrunCountRequest,
|
||||
ReturnIopOverrunCountResponse,
|
||||
ReturnQueryDataRequest,
|
||||
ReturnQueryDataResponse,
|
||||
ReturnSlaveBusCharacterOverrunCountRequest,
|
||||
ReturnSlaveBusCharacterOverrunCountResponse,
|
||||
ReturnSlaveBusyCountRequest,
|
||||
ReturnSlaveBusyCountResponse,
|
||||
ReturnSlaveMessageCountRequest,
|
||||
ReturnSlaveMessageCountResponse,
|
||||
ReturnSlaveNAKCountRequest,
|
||||
ReturnSlaveNAKCountResponse,
|
||||
ReturnSlaveNoResponseCountRequest,
|
||||
ReturnSlaveNoResponseCountResponse,
|
||||
)
|
||||
from pymodbus.exceptions import MessageRegisterException, ModbusException
|
||||
from pymodbus.file_message import (
|
||||
ReadFifoQueueRequest,
|
||||
ReadFifoQueueResponse,
|
||||
ReadFileRecordRequest,
|
||||
ReadFileRecordResponse,
|
||||
WriteFileRecordRequest,
|
||||
WriteFileRecordResponse,
|
||||
)
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.mei_message import (
|
||||
ReadDeviceInformationRequest,
|
||||
ReadDeviceInformationResponse,
|
||||
)
|
||||
from pymodbus.other_message import (
|
||||
GetCommEventCounterRequest,
|
||||
GetCommEventCounterResponse,
|
||||
GetCommEventLogRequest,
|
||||
GetCommEventLogResponse,
|
||||
ReadExceptionStatusRequest,
|
||||
ReadExceptionStatusResponse,
|
||||
ReportSlaveIdRequest,
|
||||
ReportSlaveIdResponse,
|
||||
)
|
||||
from pymodbus.pdu import (
|
||||
ExceptionResponse,
|
||||
IllegalFunctionRequest,
|
||||
ModbusRequest,
|
||||
ModbusResponse,
|
||||
)
|
||||
from pymodbus.pdu import ModbusExceptions as ecode
|
||||
from pymodbus.register_read_message import (
|
||||
ReadHoldingRegistersRequest,
|
||||
ReadHoldingRegistersResponse,
|
||||
ReadInputRegistersRequest,
|
||||
ReadInputRegistersResponse,
|
||||
ReadWriteMultipleRegistersRequest,
|
||||
ReadWriteMultipleRegistersResponse,
|
||||
)
|
||||
from pymodbus.register_write_message import (
|
||||
MaskWriteRegisterRequest,
|
||||
MaskWriteRegisterResponse,
|
||||
WriteMultipleRegistersRequest,
|
||||
WriteMultipleRegistersResponse,
|
||||
WriteSingleRegisterRequest,
|
||||
WriteSingleRegisterResponse,
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Server Decoder
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ServerDecoder:
|
||||
"""Request Message Factory (Server).
|
||||
|
||||
To add more implemented functions, simply add them to the list
|
||||
"""
|
||||
|
||||
__function_table = [
|
||||
ReadHoldingRegistersRequest,
|
||||
ReadDiscreteInputsRequest,
|
||||
ReadInputRegistersRequest,
|
||||
ReadCoilsRequest,
|
||||
WriteMultipleCoilsRequest,
|
||||
WriteMultipleRegistersRequest,
|
||||
WriteSingleRegisterRequest,
|
||||
WriteSingleCoilRequest,
|
||||
ReadWriteMultipleRegistersRequest,
|
||||
DiagnosticStatusRequest,
|
||||
ReadExceptionStatusRequest,
|
||||
GetCommEventCounterRequest,
|
||||
GetCommEventLogRequest,
|
||||
ReportSlaveIdRequest,
|
||||
ReadFileRecordRequest,
|
||||
WriteFileRecordRequest,
|
||||
MaskWriteRegisterRequest,
|
||||
ReadFifoQueueRequest,
|
||||
ReadDeviceInformationRequest,
|
||||
]
|
||||
__sub_function_table = [
|
||||
ReturnQueryDataRequest,
|
||||
RestartCommunicationsOptionRequest,
|
||||
ReturnDiagnosticRegisterRequest,
|
||||
ChangeAsciiInputDelimiterRequest,
|
||||
ForceListenOnlyModeRequest,
|
||||
ClearCountersRequest,
|
||||
ReturnBusMessageCountRequest,
|
||||
ReturnBusCommunicationErrorCountRequest,
|
||||
ReturnBusExceptionErrorCountRequest,
|
||||
ReturnSlaveMessageCountRequest,
|
||||
ReturnSlaveNoResponseCountRequest,
|
||||
ReturnSlaveNAKCountRequest,
|
||||
ReturnSlaveBusyCountRequest,
|
||||
ReturnSlaveBusCharacterOverrunCountRequest,
|
||||
ReturnIopOverrunCountRequest,
|
||||
ClearOverrunCountRequest,
|
||||
GetClearModbusPlusRequest,
|
||||
ReadDeviceInformationRequest,
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def getFCdict(cls):
|
||||
"""Build function code - class list."""
|
||||
return {f.function_code: f for f in cls.__function_table}
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the client lookup tables."""
|
||||
functions = {f.function_code for f in self.__function_table}
|
||||
self.lookup = self.getFCdict()
|
||||
self.__sub_lookup = {f: {} for f in functions}
|
||||
for f in self.__sub_function_table:
|
||||
self.__sub_lookup[f.function_code][f.sub_function_code] = f
|
||||
|
||||
def decode(self, message):
|
||||
"""Decode a request packet
|
||||
|
||||
:param message: The raw modbus request packet
|
||||
:return: The decoded modbus message or None if error
|
||||
"""
|
||||
try:
|
||||
return self._helper(message)
|
||||
except ModbusException as exc:
|
||||
Log.warning("Unable to decode request {}", exc)
|
||||
return None
|
||||
|
||||
def lookupPduClass(self, function_code):
|
||||
"""Use `function_code` to determine the class of the PDU.
|
||||
|
||||
:param function_code: The function code specified in a frame.
|
||||
:returns: The class of the PDU that has a matching `function_code`.
|
||||
"""
|
||||
return self.lookup.get(function_code, ExceptionResponse)
|
||||
|
||||
def _helper(self, data):
|
||||
"""Generate the correct request object from a valid request packet.
|
||||
|
||||
This decodes from a list of the currently implemented request types.
|
||||
|
||||
:param data: The request packet to decode
|
||||
:returns: The decoded request or illegal function request object
|
||||
"""
|
||||
function_code = int(data[0])
|
||||
if not (request := self.lookup.get(function_code, lambda: None)()):
|
||||
Log.debug("Factory Request[{}]", function_code)
|
||||
request = IllegalFunctionRequest(function_code)
|
||||
else:
|
||||
fc_string = "{}: {}".format( # pylint: disable=consider-using-f-string
|
||||
str(self.lookup[function_code]) # pylint: disable=use-maxsplit-arg
|
||||
.split(".")[-1]
|
||||
.rstrip('">"'),
|
||||
function_code,
|
||||
)
|
||||
Log.debug("Factory Request[{}]", fc_string)
|
||||
request.decode(data[1:])
|
||||
|
||||
if hasattr(request, "sub_function_code"):
|
||||
lookup = self.__sub_lookup.get(request.function_code, {})
|
||||
if subtype := lookup.get(request.sub_function_code, None):
|
||||
request.__class__ = subtype
|
||||
|
||||
return request
|
||||
|
||||
def register(self, function=None):
|
||||
"""Register a function and sub function class with the decoder.
|
||||
|
||||
:param function: Custom function class to register
|
||||
:raises MessageRegisterException:
|
||||
"""
|
||||
if function and not issubclass(function, ModbusRequest):
|
||||
raise MessageRegisterException(
|
||||
f'"{function.__class__.__name__}" is Not a valid Modbus Message'
|
||||
". Class needs to be derived from "
|
||||
"`pymodbus.pdu.ModbusRequest` "
|
||||
)
|
||||
self.lookup[function.function_code] = function
|
||||
if hasattr(function, "sub_function_code"):
|
||||
if function.function_code not in self.__sub_lookup:
|
||||
self.__sub_lookup[function.function_code] = {}
|
||||
self.__sub_lookup[function.function_code][
|
||||
function.sub_function_code
|
||||
] = function
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Client Decoder
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ClientDecoder:
|
||||
"""Response Message Factory (Client).
|
||||
|
||||
To add more implemented functions, simply add them to the list
|
||||
"""
|
||||
|
||||
function_table = [
|
||||
ReadHoldingRegistersResponse,
|
||||
ReadDiscreteInputsResponse,
|
||||
ReadInputRegistersResponse,
|
||||
ReadCoilsResponse,
|
||||
WriteMultipleCoilsResponse,
|
||||
WriteMultipleRegistersResponse,
|
||||
WriteSingleRegisterResponse,
|
||||
WriteSingleCoilResponse,
|
||||
ReadWriteMultipleRegistersResponse,
|
||||
DiagnosticStatusResponse,
|
||||
ReadExceptionStatusResponse,
|
||||
GetCommEventCounterResponse,
|
||||
GetCommEventLogResponse,
|
||||
ReportSlaveIdResponse,
|
||||
ReadFileRecordResponse,
|
||||
WriteFileRecordResponse,
|
||||
MaskWriteRegisterResponse,
|
||||
ReadFifoQueueResponse,
|
||||
ReadDeviceInformationResponse,
|
||||
]
|
||||
__sub_function_table = [
|
||||
ReturnQueryDataResponse,
|
||||
RestartCommunicationsOptionResponse,
|
||||
ReturnDiagnosticRegisterResponse,
|
||||
ChangeAsciiInputDelimiterResponse,
|
||||
ForceListenOnlyModeResponse,
|
||||
ClearCountersResponse,
|
||||
ReturnBusMessageCountResponse,
|
||||
ReturnBusCommunicationErrorCountResponse,
|
||||
ReturnBusExceptionErrorCountResponse,
|
||||
ReturnSlaveMessageCountResponse,
|
||||
ReturnSlaveNoResponseCountResponse,
|
||||
ReturnSlaveNAKCountResponse,
|
||||
ReturnSlaveBusyCountResponse,
|
||||
ReturnSlaveBusCharacterOverrunCountResponse,
|
||||
ReturnIopOverrunCountResponse,
|
||||
ClearOverrunCountResponse,
|
||||
GetClearModbusPlusResponse,
|
||||
ReadDeviceInformationResponse,
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the client lookup tables."""
|
||||
functions = {f.function_code for f in self.function_table}
|
||||
self.lookup = {f.function_code: f for f in self.function_table}
|
||||
self.__sub_lookup = {f: {} for f in functions}
|
||||
for f in self.__sub_function_table:
|
||||
self.__sub_lookup[f.function_code][f.sub_function_code] = f
|
||||
|
||||
def lookupPduClass(self, function_code):
|
||||
"""Use `function_code` to determine the class of the PDU.
|
||||
|
||||
:param function_code: The function code specified in a frame.
|
||||
:returns: The class of the PDU that has a matching `function_code`.
|
||||
"""
|
||||
return self.lookup.get(function_code, ExceptionResponse)
|
||||
|
||||
def decode(self, message):
|
||||
"""Decode a response packet.
|
||||
|
||||
:param message: The raw packet to decode
|
||||
:return: The decoded modbus message or None if error
|
||||
"""
|
||||
try:
|
||||
return self._helper(message)
|
||||
except ModbusException as exc:
|
||||
Log.error("Unable to decode response {}", exc)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
Log.error("General exception: {}", exc)
|
||||
return None
|
||||
|
||||
def _helper(self, data):
|
||||
"""Generate the correct response object from a valid response packet.
|
||||
|
||||
This decodes from a list of the currently implemented request types.
|
||||
|
||||
:param data: The response packet to decode
|
||||
:returns: The decoded request or an exception response object
|
||||
:raises ModbusException:
|
||||
"""
|
||||
fc_string = function_code = int(data[0])
|
||||
if function_code in self.lookup:
|
||||
fc_string = "{}: {}".format( # pylint: disable=consider-using-f-string
|
||||
str(self.lookup[function_code]) # pylint: disable=use-maxsplit-arg
|
||||
.split(".")[-1]
|
||||
.rstrip('">"'),
|
||||
function_code,
|
||||
)
|
||||
Log.debug("Factory Response[{}]", fc_string)
|
||||
response = self.lookup.get(function_code, lambda: None)()
|
||||
if function_code > 0x80:
|
||||
code = function_code & 0x7F # strip error portion
|
||||
response = ExceptionResponse(code, ecode.IllegalFunction)
|
||||
if not response:
|
||||
raise ModbusException(f"Unknown response {function_code}")
|
||||
response.decode(data[1:])
|
||||
|
||||
if hasattr(response, "sub_function_code"):
|
||||
lookup = self.__sub_lookup.get(response.function_code, {})
|
||||
if subtype := lookup.get(response.sub_function_code, None):
|
||||
response.__class__ = subtype
|
||||
|
||||
return response
|
||||
|
||||
def register(self, function):
|
||||
"""Register a function and sub function class with the decoder."""
|
||||
if function and not issubclass(function, ModbusResponse):
|
||||
raise MessageRegisterException(
|
||||
f'"{function.__class__.__name__}" is Not a valid Modbus Message'
|
||||
". Class needs to be derived from "
|
||||
"`pymodbus.pdu.ModbusResponse` "
|
||||
)
|
||||
self.lookup[function.function_code] = function
|
||||
if hasattr(function, "sub_function_code"):
|
||||
if function.function_code not in self.__sub_lookup:
|
||||
self.__sub_lookup[function.function_code] = {}
|
||||
self.__sub_lookup[function.function_code][
|
||||
function.sub_function_code
|
||||
] = function
|
||||
437
env/lib/python3.11/site-packages/pymodbus/file_message.py
vendored
Normal file
437
env/lib/python3.11/site-packages/pymodbus/file_message.py
vendored
Normal file
@@ -0,0 +1,437 @@
|
||||
"""File Record Read/Write Messages.
|
||||
|
||||
Currently none of these messages are implemented
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
"FileRecord",
|
||||
"ReadFileRecordRequest",
|
||||
"ReadFileRecordResponse",
|
||||
"WriteFileRecordRequest",
|
||||
"WriteFileRecordResponse",
|
||||
"ReadFifoQueueRequest",
|
||||
"ReadFifoQueueResponse",
|
||||
]
|
||||
|
||||
# pylint: disable=missing-type-doc
|
||||
import struct
|
||||
|
||||
from pymodbus.pdu import ModbusExceptions as merror
|
||||
from pymodbus.pdu import ModbusRequest, ModbusResponse
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# File Record Types
|
||||
# ---------------------------------------------------------------------------#
|
||||
class FileRecord: # pylint: disable=eq-without-hash
|
||||
"""Represents a file record and its relevant data."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:params reference_type: must be 0x06
|
||||
:params file_number: Indicates which file number we are reading
|
||||
:params record_number: Indicates which record in the file
|
||||
:params record_data: The actual data of the record
|
||||
:params record_length: The length in registers of the record
|
||||
:params response_length: The length in bytes of the record
|
||||
"""
|
||||
self.reference_type = kwargs.get("reference_type", 0x06)
|
||||
self.file_number = kwargs.get("file_number", 0x00)
|
||||
self.record_number = kwargs.get("record_number", 0x00)
|
||||
self.record_data = kwargs.get("record_data", "")
|
||||
|
||||
self.record_length = kwargs.get("record_length", len(self.record_data) // 2)
|
||||
self.response_length = kwargs.get("response_length", len(self.record_data) + 1)
|
||||
|
||||
def __eq__(self, relf):
|
||||
"""Compare the left object to the right."""
|
||||
return (
|
||||
self.reference_type == relf.reference_type
|
||||
and self.file_number == relf.file_number
|
||||
and self.record_number == relf.record_number
|
||||
and self.record_length == relf.record_length
|
||||
and self.record_data == relf.record_data
|
||||
)
|
||||
|
||||
def __ne__(self, relf):
|
||||
"""Compare the left object to the right."""
|
||||
return not self.__eq__(relf)
|
||||
|
||||
def __repr__(self):
|
||||
"""Give a representation of the file record."""
|
||||
params = (self.file_number, self.record_number, self.record_length)
|
||||
return (
|
||||
"FileRecord(file=%d, record=%d, length=%d)" # pylint: disable=consider-using-f-string
|
||||
% params
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# File Requests/Responses
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ReadFileRecordRequest(ModbusRequest):
|
||||
"""Read file record request.
|
||||
|
||||
This function code is used to perform a file record read. All request
|
||||
data lengths are provided in terms of number of bytes and all record
|
||||
lengths are provided in terms of registers.
|
||||
|
||||
A file is an organization of records. Each file contains 10000 records,
|
||||
addressed 0000 to 9999 decimal or 0x0000 to 0x270f. For example, record
|
||||
12 is addressed as 12. The function can read multiple groups of
|
||||
references. The groups can be separating (non-contiguous), but the
|
||||
references within each group must be sequential. Each group is defined
|
||||
in a separate "sub-request" field that contains seven bytes::
|
||||
|
||||
The reference type: 1 byte (must be 0x06)
|
||||
The file number: 2 bytes
|
||||
The starting record number within the file: 2 bytes
|
||||
The length of the record to be read: 2 bytes
|
||||
|
||||
The quantity of registers to be read, combined with all other fields
|
||||
in the expected response, must not exceed the allowable length of the
|
||||
MODBUS PDU: 235 bytes.
|
||||
"""
|
||||
|
||||
function_code = 0x14
|
||||
function_code_name = "read_file_record"
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, records=None, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param records: The file record requests to be read
|
||||
"""
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.records = records or []
|
||||
|
||||
def encode(self):
|
||||
"""Encode the request packet.
|
||||
|
||||
:returns: The byte encoded packet
|
||||
"""
|
||||
packet = struct.pack("B", len(self.records) * 7)
|
||||
for record in self.records:
|
||||
packet += struct.pack(
|
||||
">BHHH",
|
||||
0x06,
|
||||
record.file_number,
|
||||
record.record_number,
|
||||
record.record_length,
|
||||
)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode the incoming request.
|
||||
|
||||
:param data: The data to decode into the address
|
||||
"""
|
||||
self.records = []
|
||||
byte_count = int(data[0])
|
||||
for count in range(1, byte_count, 7):
|
||||
decoded = struct.unpack(">BHHH", data[count : count + 7])
|
||||
record = FileRecord(
|
||||
file_number=decoded[1],
|
||||
record_number=decoded[2],
|
||||
record_length=decoded[3],
|
||||
)
|
||||
if decoded[0] == 0x06:
|
||||
self.records.append(record)
|
||||
|
||||
def execute(self, _context):
|
||||
"""Run a read exception status request against the store.
|
||||
|
||||
:returns: The populated response
|
||||
"""
|
||||
# TODO do some new context operation here # pylint: disable=fixme
|
||||
# if file number, record number, or address + length
|
||||
# is too big, return an error.
|
||||
files = []
|
||||
return ReadFileRecordResponse(files)
|
||||
|
||||
|
||||
class ReadFileRecordResponse(ModbusResponse):
|
||||
"""Read file record response.
|
||||
|
||||
The normal response is a series of "sub-responses," one for each
|
||||
"sub-request." The byte count field is the total combined count of
|
||||
bytes in all "sub-responses." In addition, each "sub-response"
|
||||
contains a field that shows its own byte count.
|
||||
"""
|
||||
|
||||
function_code = 0x14
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, records=None, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param records: The requested file records
|
||||
"""
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.records = records or []
|
||||
|
||||
def encode(self):
|
||||
"""Encode the response.
|
||||
|
||||
:returns: The byte encoded message
|
||||
"""
|
||||
total = sum(record.response_length + 1 for record in self.records)
|
||||
packet = struct.pack("B", total)
|
||||
for record in self.records:
|
||||
packet += struct.pack(">BB", 0x06, record.record_length)
|
||||
packet += record.record_data
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode the response.
|
||||
|
||||
:param data: The packet data to decode
|
||||
"""
|
||||
count, self.records = 1, []
|
||||
byte_count = int(data[0])
|
||||
while count < byte_count:
|
||||
response_length, reference_type = struct.unpack(
|
||||
">BB", data[count : count + 2]
|
||||
)
|
||||
count += response_length + 1 # the count is not included
|
||||
record = FileRecord(
|
||||
response_length=response_length,
|
||||
record_data=data[count - response_length + 1 : count],
|
||||
)
|
||||
if reference_type == 0x06:
|
||||
self.records.append(record)
|
||||
|
||||
|
||||
class WriteFileRecordRequest(ModbusRequest):
|
||||
"""Write file record request.
|
||||
|
||||
This function code is used to perform a file record write. All
|
||||
request data lengths are provided in terms of number of bytes
|
||||
and all record lengths are provided in terms of the number of 16
|
||||
bit words.
|
||||
"""
|
||||
|
||||
function_code = 0x15
|
||||
function_code_name = "write_file_record"
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, records=None, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param records: The file record requests to be read
|
||||
"""
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.records = records or []
|
||||
|
||||
def encode(self):
|
||||
"""Encode the request packet.
|
||||
|
||||
:returns: The byte encoded packet
|
||||
"""
|
||||
total_length = sum((record.record_length * 2) + 7 for record in self.records)
|
||||
packet = struct.pack("B", total_length)
|
||||
|
||||
for record in self.records:
|
||||
packet += struct.pack(
|
||||
">BHHH",
|
||||
0x06,
|
||||
record.file_number,
|
||||
record.record_number,
|
||||
record.record_length,
|
||||
)
|
||||
packet += record.record_data
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode the incoming request.
|
||||
|
||||
:param data: The data to decode into the address
|
||||
"""
|
||||
byte_count = int(data[0])
|
||||
count, self.records = 1, []
|
||||
while count < byte_count:
|
||||
decoded = struct.unpack(">BHHH", data[count : count + 7])
|
||||
response_length = decoded[3] * 2
|
||||
count += response_length + 7
|
||||
record = FileRecord(
|
||||
record_length=decoded[3],
|
||||
file_number=decoded[1],
|
||||
record_number=decoded[2],
|
||||
record_data=data[count - response_length : count],
|
||||
)
|
||||
if decoded[0] == 0x06:
|
||||
self.records.append(record)
|
||||
|
||||
def execute(self, _context):
|
||||
"""Run the write file record request against the context.
|
||||
|
||||
:returns: The populated response
|
||||
"""
|
||||
# TODO do some new context operation here # pylint: disable=fixme
|
||||
# if file number, record number, or address + length
|
||||
# is too big, return an error.
|
||||
return WriteFileRecordResponse(self.records)
|
||||
|
||||
|
||||
class WriteFileRecordResponse(ModbusResponse):
|
||||
"""The normal response is an echo of the request."""
|
||||
|
||||
function_code = 0x15
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, records=None, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param records: The file record requests to be read
|
||||
"""
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.records = records or []
|
||||
|
||||
def encode(self):
|
||||
"""Encode the response.
|
||||
|
||||
:returns: The byte encoded message
|
||||
"""
|
||||
total_length = sum((record.record_length * 2) + 7 for record in self.records)
|
||||
packet = struct.pack("B", total_length)
|
||||
for record in self.records:
|
||||
packet += struct.pack(
|
||||
">BHHH",
|
||||
0x06,
|
||||
record.file_number,
|
||||
record.record_number,
|
||||
record.record_length,
|
||||
)
|
||||
packet += record.record_data
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode the incoming request.
|
||||
|
||||
:param data: The data to decode into the address
|
||||
"""
|
||||
count, self.records = 1, []
|
||||
byte_count = int(data[0])
|
||||
while count < byte_count:
|
||||
decoded = struct.unpack(">BHHH", data[count : count + 7])
|
||||
response_length = decoded[3] * 2
|
||||
count += response_length + 7
|
||||
record = FileRecord(
|
||||
record_length=decoded[3],
|
||||
file_number=decoded[1],
|
||||
record_number=decoded[2],
|
||||
record_data=data[count - response_length : count],
|
||||
)
|
||||
if decoded[0] == 0x06:
|
||||
self.records.append(record)
|
||||
|
||||
|
||||
class ReadFifoQueueRequest(ModbusRequest):
|
||||
"""Read fifo queue request.
|
||||
|
||||
This function code allows to read the contents of a First-In-First-Out
|
||||
(FIFO) queue of register in a remote device. The function returns a
|
||||
count of the registers in the queue, followed by the queued data.
|
||||
Up to 32 registers can be read: the count, plus up to 31 queued data
|
||||
registers.
|
||||
|
||||
The queue count register is returned first, followed by the queued data
|
||||
registers. The function reads the queue contents, but does not clear
|
||||
them.
|
||||
"""
|
||||
|
||||
function_code = 0x18
|
||||
function_code_name = "read_fifo_queue"
|
||||
_rtu_frame_size = 6
|
||||
|
||||
def __init__(self, address=0x0000, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param address: The fifo pointer address (0x0000 to 0xffff)
|
||||
"""
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.values = [] # this should be added to the context
|
||||
|
||||
def encode(self):
|
||||
"""Encode the request packet.
|
||||
|
||||
:returns: The byte encoded packet
|
||||
"""
|
||||
return struct.pack(">H", self.address)
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode the incoming request.
|
||||
|
||||
:param data: The data to decode into the address
|
||||
"""
|
||||
self.address = struct.unpack(">H", data)[0]
|
||||
|
||||
def execute(self, _context):
|
||||
"""Run a read exception status request against the store.
|
||||
|
||||
:returns: The populated response
|
||||
"""
|
||||
if not 0x0000 <= self.address <= 0xFFFF:
|
||||
return self.doException(merror.IllegalValue)
|
||||
if len(self.values) > 31:
|
||||
return self.doException(merror.IllegalValue)
|
||||
# TODO pull the values from some context # pylint: disable=fixme
|
||||
return ReadFifoQueueResponse(self.values)
|
||||
|
||||
|
||||
class ReadFifoQueueResponse(ModbusResponse):
|
||||
"""Read Fifo queue response.
|
||||
|
||||
In a normal response, the byte count shows the quantity of bytes to
|
||||
follow, including the queue count bytes and value register bytes
|
||||
(but not including the error check field). The queue count is the
|
||||
quantity of data registers in the queue (not including the count register).
|
||||
|
||||
If the queue count exceeds 31, an exception response is returned with an
|
||||
error code of 03 (Illegal Data Value).
|
||||
"""
|
||||
|
||||
function_code = 0x18
|
||||
|
||||
@classmethod
|
||||
def calculateRtuFrameSize(cls, buffer):
|
||||
"""Calculate the size of the message.
|
||||
|
||||
:param buffer: A buffer containing the data that have been received.
|
||||
:returns: The number of bytes in the response.
|
||||
"""
|
||||
hi_byte = int(buffer[2])
|
||||
lo_byte = int(buffer[3])
|
||||
return (hi_byte << 16) + lo_byte + 6
|
||||
|
||||
def __init__(self, values=None, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param values: The list of values of the fifo to return
|
||||
"""
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.values = values or []
|
||||
|
||||
def encode(self):
|
||||
"""Encode the response.
|
||||
|
||||
:returns: The byte encoded message
|
||||
"""
|
||||
length = len(self.values) * 2
|
||||
packet = struct.pack(">HH", 2 + length, length)
|
||||
for value in self.values:
|
||||
packet += struct.pack(">H", value)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode a the response.
|
||||
|
||||
:param data: The packet data to decode
|
||||
"""
|
||||
self.values = []
|
||||
_, count = struct.unpack(">HH", data[0:4])
|
||||
for index in range(0, count - 4):
|
||||
idx = 4 + index * 2
|
||||
self.values.append(struct.unpack(">H", data[idx : idx + 2])[0])
|
||||
17
env/lib/python3.11/site-packages/pymodbus/framer/__init__.py
vendored
Normal file
17
env/lib/python3.11/site-packages/pymodbus/framer/__init__.py
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Framer"""
|
||||
|
||||
__all__ = [
|
||||
"ModbusFramer",
|
||||
"ModbusAsciiFramer",
|
||||
"ModbusBinaryFramer",
|
||||
"ModbusRtuFramer",
|
||||
"ModbusSocketFramer",
|
||||
"ModbusTlsFramer",
|
||||
]
|
||||
|
||||
from pymodbus.framer.ascii_framer import ModbusAsciiFramer
|
||||
from pymodbus.framer.base import ModbusFramer
|
||||
from pymodbus.framer.binary_framer import ModbusBinaryFramer
|
||||
from pymodbus.framer.rtu_framer import ModbusRtuFramer
|
||||
from pymodbus.framer.socket_framer import ModbusSocketFramer
|
||||
from pymodbus.framer.tls_framer import ModbusTlsFramer
|
||||
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/ascii_framer.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/ascii_framer.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/base.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/base.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/binary_framer.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/binary_framer.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/rtu_framer.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/rtu_framer.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/socket_framer.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/socket_framer.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/tls_framer.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/tls_framer.cpython-311.pyc
vendored
Normal file
Binary file not shown.
152
env/lib/python3.11/site-packages/pymodbus/framer/ascii_framer.py
vendored
Normal file
152
env/lib/python3.11/site-packages/pymodbus/framer/ascii_framer.py
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
"""Ascii_framer."""
|
||||
# pylint: disable=missing-type-doc
|
||||
import struct
|
||||
from binascii import a2b_hex, b2a_hex
|
||||
|
||||
from pymodbus.exceptions import ModbusIOException
|
||||
from pymodbus.framer.base import BYTE_ORDER, FRAME_HEADER, ModbusFramer
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.utilities import checkLRC, computeLRC
|
||||
|
||||
|
||||
ASCII_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus ASCII Message
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusAsciiFramer(ModbusFramer):
|
||||
r"""Modbus ASCII Frame Controller.
|
||||
|
||||
[ Start ][Address ][ Function ][ Data ][ LRC ][ End ]
|
||||
1c 2c 2c Nc 2c 2c
|
||||
|
||||
* data can be 0 - 2x252 chars
|
||||
* end is "\\r\\n" (Carriage return line feed), however the line feed
|
||||
character can be changed via a special command
|
||||
* start is ":"
|
||||
|
||||
This framer is used for serial transmission. Unlike the RTU protocol,
|
||||
the data in this framer is transferred in plain text ascii.
|
||||
"""
|
||||
|
||||
method = "ascii"
|
||||
|
||||
def __init__(self, decoder, client=None):
|
||||
"""Initialize a new instance of the framer.
|
||||
|
||||
:param decoder: The decoder implementation to use
|
||||
"""
|
||||
super().__init__(decoder, client)
|
||||
self._hsize = 0x02
|
||||
self._start = b":"
|
||||
self._end = b"\r\n"
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Private Helper Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def decode_data(self, data):
|
||||
"""Decode data."""
|
||||
if len(data) > 1:
|
||||
uid = int(data[1:3], 16)
|
||||
fcode = int(data[3:5], 16)
|
||||
return {"slave": uid, "fcode": fcode}
|
||||
return {}
|
||||
|
||||
def checkFrame(self):
|
||||
"""Check and decode the next frame.
|
||||
|
||||
:returns: True if we successful, False otherwise
|
||||
"""
|
||||
start = self._buffer.find(self._start)
|
||||
if start == -1:
|
||||
return False
|
||||
if start > 0: # go ahead and skip old bad data
|
||||
self._buffer = self._buffer[start:]
|
||||
start = 0
|
||||
|
||||
if (end := self._buffer.find(self._end)) != -1:
|
||||
self._header["len"] = end
|
||||
self._header["uid"] = int(self._buffer[1:3], 16)
|
||||
self._header["lrc"] = int(self._buffer[end - 2 : end], 16)
|
||||
data = a2b_hex(self._buffer[start + 1 : end - 2])
|
||||
return checkLRC(data, self._header["lrc"])
|
||||
return False
|
||||
|
||||
def advanceFrame(self):
|
||||
"""Skip over the current framed message.
|
||||
|
||||
This allows us to skip over the current message after we have processed
|
||||
it or determined that it contains an error. It also has to reset the
|
||||
current frame header handle
|
||||
"""
|
||||
self._buffer = self._buffer[self._header["len"] + 2 :]
|
||||
self._header = {"lrc": "0000", "len": 0, "uid": 0x00}
|
||||
|
||||
def isFrameReady(self):
|
||||
"""Check if we should continue decode logic.
|
||||
|
||||
This is meant to be used in a while loop in the decoding phase to let
|
||||
the decoder know that there is still data in the buffer.
|
||||
|
||||
:returns: True if ready, False otherwise
|
||||
"""
|
||||
return len(self._buffer) > 1
|
||||
|
||||
def getFrame(self):
|
||||
"""Get the next frame from the buffer.
|
||||
|
||||
:returns: The frame data or ""
|
||||
"""
|
||||
start = self._hsize + 1
|
||||
end = self._header["len"] - 2
|
||||
buffer = self._buffer[start:end]
|
||||
if end > 0:
|
||||
return a2b_hex(buffer)
|
||||
return b""
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Public Member Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def frameProcessIncomingPacket(self, single, callback, slave, _tid=None, **kwargs):
|
||||
"""Process new packet pattern."""
|
||||
while self.isFrameReady():
|
||||
if not self.checkFrame():
|
||||
break
|
||||
if not self._validate_slave_id(slave, single):
|
||||
header_txt = self._header["uid"]
|
||||
Log.error("Not a valid slave id - {}, ignoring!!", header_txt)
|
||||
self.resetFrame()
|
||||
continue
|
||||
|
||||
frame = self.getFrame()
|
||||
if (result := self.decoder.decode(frame)) is None:
|
||||
raise ModbusIOException("Unable to decode response")
|
||||
self.populateResult(result)
|
||||
self.advanceFrame()
|
||||
callback(result) # defer this
|
||||
|
||||
def buildPacket(self, message):
|
||||
"""Create a ready to send modbus packet.
|
||||
|
||||
Built off of a modbus request/response
|
||||
|
||||
:param message: The request/response to send
|
||||
:return: The encoded packet
|
||||
"""
|
||||
encoded = message.encode()
|
||||
buffer = struct.pack(
|
||||
ASCII_FRAME_HEADER, message.slave_id, message.function_code
|
||||
)
|
||||
checksum = computeLRC(encoded + buffer)
|
||||
|
||||
packet = bytearray()
|
||||
packet.extend(self._start)
|
||||
packet.extend(f"{message.slave_id:02x}{message.function_code:02x}".encode())
|
||||
packet.extend(b2a_hex(encoded))
|
||||
packet.extend(f"{checksum:02x}".encode())
|
||||
packet.extend(self._end)
|
||||
return bytes(packet).upper()
|
||||
|
||||
|
||||
# __END__
|
||||
142
env/lib/python3.11/site-packages/pymodbus/framer/base.py
vendored
Normal file
142
env/lib/python3.11/site-packages/pymodbus/framer/base.py
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
"""Framer start."""
|
||||
# pylint: disable=missing-type-doc
|
||||
from typing import Any, Dict, Union
|
||||
|
||||
from pymodbus.factory import ClientDecoder, ServerDecoder
|
||||
from pymodbus.logging import Log
|
||||
|
||||
|
||||
# Unit ID, Function Code
|
||||
BYTE_ORDER = ">"
|
||||
FRAME_HEADER = "BB"
|
||||
|
||||
# Transaction Id, Protocol ID, Length, Unit ID, Function Code
|
||||
SOCKET_FRAME_HEADER = BYTE_ORDER + "HHH" + FRAME_HEADER
|
||||
|
||||
# Function Code
|
||||
TLS_FRAME_HEADER = BYTE_ORDER + "B"
|
||||
|
||||
|
||||
class ModbusFramer:
|
||||
"""Base Framer class."""
|
||||
|
||||
name = ""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
decoder: Union[ClientDecoder, ServerDecoder],
|
||||
client=None,
|
||||
) -> None:
|
||||
"""Initialize a new instance of the framer.
|
||||
|
||||
:param decoder: The decoder implementation to use
|
||||
"""
|
||||
self.decoder = decoder
|
||||
self.client = client
|
||||
self._header: Dict[str, Any] = {
|
||||
"lrc": "0000",
|
||||
"len": 0,
|
||||
"uid": 0x00,
|
||||
"tid": 0,
|
||||
"pid": 0,
|
||||
"crc": b"\x00\x00",
|
||||
}
|
||||
self._buffer = b""
|
||||
|
||||
def _validate_slave_id(self, slaves: list, single: bool) -> bool:
|
||||
"""Validate if the received data is valid for the client.
|
||||
|
||||
:param slaves: list of slave id for which the transaction is valid
|
||||
:param single: Set to true to treat this as a single context
|
||||
:return:
|
||||
"""
|
||||
if single:
|
||||
return True
|
||||
if 0 in slaves or 0xFF in slaves:
|
||||
# Handle Modbus TCP slave identifier (0x00 0r 0xFF)
|
||||
# in asynchronous requests
|
||||
return True
|
||||
return self._header["uid"] in slaves
|
||||
|
||||
def sendPacket(self, message):
|
||||
"""Send packets on the bus.
|
||||
|
||||
With 3.5char delay between frames
|
||||
:param message: Message to be sent over the bus
|
||||
:return:
|
||||
"""
|
||||
return self.client.send(message)
|
||||
|
||||
def recvPacket(self, size):
|
||||
"""Receive packet from the bus.
|
||||
|
||||
With specified len
|
||||
:param size: Number of bytes to read
|
||||
:return:
|
||||
"""
|
||||
return self.client.recv(size)
|
||||
|
||||
def resetFrame(self):
|
||||
"""Reset the entire message frame.
|
||||
|
||||
This allows us to skip ovver errors that may be in the stream.
|
||||
It is hard to know if we are simply out of sync or if there is
|
||||
an error in the stream as we have no way to check the start or
|
||||
end of the message (python just doesn't have the resolution to
|
||||
check for millisecond delays).
|
||||
"""
|
||||
Log.debug(
|
||||
"Resetting frame - Current Frame in buffer - {}", self._buffer, ":hex"
|
||||
)
|
||||
self._buffer = b""
|
||||
self._header = {
|
||||
"lrc": "0000",
|
||||
"crc": b"\x00\x00",
|
||||
"len": 0,
|
||||
"uid": 0x00,
|
||||
"pid": 0,
|
||||
"tid": 0,
|
||||
}
|
||||
|
||||
def populateResult(self, result):
|
||||
"""Populate the modbus result header.
|
||||
|
||||
The serial packets do not have any header information
|
||||
that is copied.
|
||||
|
||||
:param result: The response packet
|
||||
"""
|
||||
result.slave_id = self._header.get("uid", 0)
|
||||
result.transaction_id = self._header.get("tid", 0)
|
||||
result.protocol_id = self._header.get("pid", 0)
|
||||
|
||||
def processIncomingPacket(self, data, callback, slave, **kwargs):
|
||||
"""Process new packet pattern.
|
||||
|
||||
This takes in a new request packet, adds it to the current
|
||||
packet stream, and performs framing on it. That is, checks
|
||||
for complete messages, and once found, will process all that
|
||||
exist. This handles the case when we read N + 1 or 1 // N
|
||||
messages at a time instead of 1.
|
||||
|
||||
The processed and decoded messages are pushed to the callback
|
||||
function to process and send.
|
||||
|
||||
:param data: The new packet data
|
||||
:param callback: The function to send results to
|
||||
:param slave: Process if slave id matches, ignore otherwise (could be a
|
||||
list of slave ids (server) or single slave id(client/server))
|
||||
:param kwargs:
|
||||
:raises ModbusIOException:
|
||||
"""
|
||||
Log.debug("Processing: {}", data, ":hex")
|
||||
self._buffer += data
|
||||
if not isinstance(slave, (list, tuple)):
|
||||
slave = [slave]
|
||||
single = kwargs.pop("single", False)
|
||||
self.frameProcessIncomingPacket(single, callback, slave, **kwargs)
|
||||
|
||||
def frameProcessIncomingPacket(
|
||||
self, _single, _callback, _slave, _tid=None, **kwargs
|
||||
):
|
||||
"""Process new packet pattern."""
|
||||
171
env/lib/python3.11/site-packages/pymodbus/framer/binary_framer.py
vendored
Normal file
171
env/lib/python3.11/site-packages/pymodbus/framer/binary_framer.py
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
"""Binary framer."""
|
||||
# pylint: disable=missing-type-doc
|
||||
import struct
|
||||
|
||||
from pymodbus.exceptions import ModbusIOException
|
||||
from pymodbus.framer.base import BYTE_ORDER, FRAME_HEADER, ModbusFramer
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.utilities import checkCRC, computeCRC
|
||||
|
||||
|
||||
BINARY_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus Binary Message
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class ModbusBinaryFramer(ModbusFramer):
|
||||
"""Modbus Binary Frame Controller.
|
||||
|
||||
[ Start ][Address ][ Function ][ Data ][ CRC ][ End ]
|
||||
1b 1b 1b Nb 2b 1b
|
||||
|
||||
* data can be 0 - 2x252 chars
|
||||
* end is "}"
|
||||
* start is "{"
|
||||
|
||||
The idea here is that we implement the RTU protocol, however,
|
||||
instead of using timing for message delimiting, we use start
|
||||
and end of message characters (in this case { and }). Basically,
|
||||
this is a binary framer.
|
||||
|
||||
The only case we have to watch out for is when a message contains
|
||||
the { or } characters. If we encounter these characters, we
|
||||
simply duplicate them. Hopefully we will not encounter those
|
||||
characters that often and will save a little bit of bandwitch
|
||||
without a real-time system.
|
||||
|
||||
Protocol defined by jamod.sourceforge.net.
|
||||
"""
|
||||
|
||||
method = "binary"
|
||||
|
||||
def __init__(self, decoder, client=None):
|
||||
"""Initialize a new instance of the framer.
|
||||
|
||||
:param decoder: The decoder implementation to use
|
||||
"""
|
||||
super().__init__(decoder, client)
|
||||
# self._header.update({"crc": 0x0000})
|
||||
self._hsize = 0x01
|
||||
self._start = b"\x7b" # {
|
||||
self._end = b"\x7d" # }
|
||||
self._repeat = [b"}"[0], b"{"[0]] # python3 hack
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Private Helper Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def decode_data(self, data):
|
||||
"""Decode data."""
|
||||
if len(data) > self._hsize:
|
||||
uid = struct.unpack(">B", data[1:2])[0]
|
||||
fcode = struct.unpack(">B", data[2:3])[0]
|
||||
return {"slave": uid, "fcode": fcode}
|
||||
return {}
|
||||
|
||||
def checkFrame(self) -> bool:
|
||||
"""Check and decode the next frame.
|
||||
|
||||
:returns: True if we are successful, False otherwise
|
||||
"""
|
||||
start = self._buffer.find(self._start)
|
||||
if start == -1:
|
||||
return False
|
||||
if start > 0: # go ahead and skip old bad data
|
||||
self._buffer = self._buffer[start:]
|
||||
|
||||
if (end := self._buffer.find(self._end)) != -1:
|
||||
self._header["len"] = end
|
||||
self._header["uid"] = struct.unpack(">B", self._buffer[1:2])[0]
|
||||
self._header["crc"] = struct.unpack(">H", self._buffer[end - 2 : end])[0]
|
||||
data = self._buffer[start + 1 : end - 2]
|
||||
return checkCRC(data, self._header["crc"])
|
||||
return False
|
||||
|
||||
def advanceFrame(self) -> None:
|
||||
"""Skip over the current framed message.
|
||||
|
||||
This allows us to skip over the current message after we have processed
|
||||
it or determined that it contains an error. It also has to reset the
|
||||
current frame header handle
|
||||
"""
|
||||
self._buffer = self._buffer[self._header["len"] + 2 :]
|
||||
self._header = {"crc": 0x0000, "len": 0, "uid": 0x00}
|
||||
|
||||
def isFrameReady(self) -> bool:
|
||||
"""Check if we should continue decode logic.
|
||||
|
||||
This is meant to be used in a while loop in the decoding phase to let
|
||||
the decoder know that there is still data in the buffer.
|
||||
|
||||
:returns: True if ready, False otherwise
|
||||
"""
|
||||
return len(self._buffer) > 1
|
||||
|
||||
def getFrame(self):
|
||||
"""Get the next frame from the buffer.
|
||||
|
||||
:returns: The frame data or ""
|
||||
"""
|
||||
start = self._hsize + 1
|
||||
end = self._header["len"] - 2
|
||||
buffer = self._buffer[start:end]
|
||||
if end > 0:
|
||||
return buffer
|
||||
return b""
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Public Member Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def frameProcessIncomingPacket(self, single, callback, slave, _tid=None, **kwargs):
|
||||
"""Process new packet pattern."""
|
||||
while self.isFrameReady():
|
||||
if not self.checkFrame():
|
||||
Log.debug("Frame check failed, ignoring!!")
|
||||
self.resetFrame()
|
||||
break
|
||||
if not self._validate_slave_id(slave, single):
|
||||
header_txt = self._header["uid"]
|
||||
Log.debug("Not a valid slave id - {}, ignoring!!", header_txt)
|
||||
self.resetFrame()
|
||||
break
|
||||
if (result := self.decoder.decode(self.getFrame())) is None:
|
||||
raise ModbusIOException("Unable to decode response")
|
||||
self.populateResult(result)
|
||||
self.advanceFrame()
|
||||
callback(result) # defer or push to a thread?
|
||||
|
||||
def buildPacket(self, message):
|
||||
"""Create a ready to send modbus packet.
|
||||
|
||||
:param message: The request/response to send
|
||||
:returns: The encoded packet
|
||||
"""
|
||||
data = self._preflight(message.encode())
|
||||
packet = (
|
||||
struct.pack(BINARY_FRAME_HEADER, message.slave_id, message.function_code)
|
||||
+ data
|
||||
)
|
||||
packet += struct.pack(">H", computeCRC(packet))
|
||||
packet = self._start + packet + self._end
|
||||
return packet
|
||||
|
||||
def _preflight(self, data):
|
||||
"""Do preflight buffer test.
|
||||
|
||||
This basically scans the buffer for start and end
|
||||
tags and if found, escapes them.
|
||||
|
||||
:param data: The message to escape
|
||||
:returns: the escaped packet
|
||||
"""
|
||||
array = bytearray()
|
||||
for item in data:
|
||||
if item in self._repeat:
|
||||
array.append(item)
|
||||
array.append(item)
|
||||
return bytes(array)
|
||||
|
||||
|
||||
# __END__
|
||||
323
env/lib/python3.11/site-packages/pymodbus/framer/rtu_framer.py
vendored
Normal file
323
env/lib/python3.11/site-packages/pymodbus/framer/rtu_framer.py
vendored
Normal file
@@ -0,0 +1,323 @@
|
||||
"""RTU framer."""
|
||||
# pylint: disable=missing-type-doc
|
||||
import struct
|
||||
import time
|
||||
|
||||
from pymodbus.exceptions import (
|
||||
InvalidMessageReceivedException,
|
||||
ModbusIOException,
|
||||
)
|
||||
from pymodbus.framer.base import BYTE_ORDER, FRAME_HEADER, ModbusFramer
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.utilities import ModbusTransactionState, checkCRC, computeCRC
|
||||
|
||||
|
||||
RTU_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus RTU Message
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusRtuFramer(ModbusFramer):
|
||||
"""Modbus RTU Frame controller.
|
||||
|
||||
[ Start Wait ] [Address ][ Function Code] [ Data ][ CRC ][ End Wait ]
|
||||
3.5 chars 1b 1b Nb 2b 3.5 chars
|
||||
|
||||
Wait refers to the amount of time required to transmit at least x many
|
||||
characters. In this case it is 3.5 characters. Also, if we receive a
|
||||
wait of 1.5 characters at any point, we must trigger an error message.
|
||||
Also, it appears as though this message is little endian. The logic is
|
||||
simplified as the following::
|
||||
|
||||
block-on-read:
|
||||
read until 3.5 delay
|
||||
check for errors
|
||||
decode
|
||||
|
||||
The following table is a listing of the baud wait times for the specified
|
||||
baud rates::
|
||||
|
||||
------------------------------------------------------------------
|
||||
Baud 1.5c (18 bits) 3.5c (38 bits)
|
||||
------------------------------------------------------------------
|
||||
1200 13333.3 us 31666.7 us
|
||||
4800 3333.3 us 7916.7 us
|
||||
9600 1666.7 us 3958.3 us
|
||||
19200 833.3 us 1979.2 us
|
||||
38400 416.7 us 989.6 us
|
||||
------------------------------------------------------------------
|
||||
1 Byte = start + 8 bits + parity + stop = 11 bits
|
||||
(1/Baud)(bits) = delay seconds
|
||||
"""
|
||||
|
||||
method = "rtu"
|
||||
|
||||
def __init__(self, decoder, client=None):
|
||||
"""Initialize a new instance of the framer.
|
||||
|
||||
:param decoder: The decoder factory implementation to use
|
||||
"""
|
||||
super().__init__(decoder, client)
|
||||
self._hsize = 0x01
|
||||
self._end = b"\x0d\x0a"
|
||||
self._min_frame_size = 4
|
||||
self.function_codes = decoder.lookup.keys() if decoder else {}
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Private Helper Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def decode_data(self, data):
|
||||
"""Decode data."""
|
||||
if len(data) > self._hsize:
|
||||
uid = int(data[0])
|
||||
fcode = int(data[1])
|
||||
return {"slave": uid, "fcode": fcode}
|
||||
return {}
|
||||
|
||||
def checkFrame(self):
|
||||
"""Check if the next frame is available.
|
||||
|
||||
Return True if we were successful.
|
||||
|
||||
1. Populate header
|
||||
2. Discard frame if UID does not match
|
||||
"""
|
||||
try:
|
||||
self.populateHeader()
|
||||
frame_size = self._header["len"]
|
||||
data = self._buffer[: frame_size - 2]
|
||||
crc = self._header["crc"]
|
||||
crc_val = (int(crc[0]) << 8) + int(crc[1])
|
||||
return checkCRC(data, crc_val)
|
||||
except (IndexError, KeyError, struct.error):
|
||||
return False
|
||||
|
||||
def advanceFrame(self):
|
||||
"""Skip over the current framed message.
|
||||
|
||||
This allows us to skip over the current message after we have processed
|
||||
it or determined that it contains an error. It also has to reset the
|
||||
current frame header handle
|
||||
"""
|
||||
self._buffer = self._buffer[self._header["len"] :]
|
||||
Log.debug("Frame advanced, resetting header!!")
|
||||
self._header = {"uid": 0x00, "len": 0, "crc": b"\x00\x00"}
|
||||
|
||||
def resetFrame(self):
|
||||
"""Reset the entire message frame.
|
||||
|
||||
This allows us to skip over errors that may be in the stream.
|
||||
It is hard to know if we are simply out of sync or if there is
|
||||
an error in the stream as we have no way to check the start or
|
||||
end of the message (python just doesn't have the resolution to
|
||||
check for millisecond delays).
|
||||
"""
|
||||
x = self._buffer
|
||||
super().resetFrame()
|
||||
self._buffer = x
|
||||
|
||||
def isFrameReady(self):
|
||||
"""Check if we should continue decode logic.
|
||||
|
||||
This is meant to be used in a while loop in the decoding phase to let
|
||||
the decoder know that there is still data in the buffer.
|
||||
|
||||
:returns: True if ready, False otherwise
|
||||
"""
|
||||
size = self._header.get("len", 0)
|
||||
if not size and len(self._buffer) > self._hsize:
|
||||
try:
|
||||
# Frame is ready only if populateHeader() successfully
|
||||
# populates crc field which finishes RTU frame otherwise,
|
||||
# if buffer is not yet long enough, populateHeader() raises IndexError
|
||||
size = self.populateHeader()
|
||||
except IndexError:
|
||||
return False
|
||||
|
||||
return len(self._buffer) >= size if size > 0 else False
|
||||
|
||||
def populateHeader(self, data=None):
|
||||
"""Try to set the headers `uid`, `len` and `crc`.
|
||||
|
||||
This method examines `self._buffer` and writes meta
|
||||
information into `self._header`.
|
||||
|
||||
Beware that this method will raise an IndexError if
|
||||
`self._buffer` is not yet long enough.
|
||||
"""
|
||||
data = data if data is not None else self._buffer
|
||||
self._header["uid"] = int(data[0])
|
||||
self._header["tid"] = int(data[0])
|
||||
size = self.get_expected_response_length(data)
|
||||
self._header["len"] = size
|
||||
|
||||
if len(data) < size:
|
||||
# crc yet not available
|
||||
raise IndexError
|
||||
self._header["crc"] = data[size - 2 : size]
|
||||
return size
|
||||
|
||||
def getFrame(self):
|
||||
"""Get the next frame from the buffer.
|
||||
|
||||
:returns: The frame data or ""
|
||||
"""
|
||||
start = self._hsize
|
||||
end = self._header["len"] - 2
|
||||
buffer = self._buffer[start:end]
|
||||
if end > 0:
|
||||
Log.debug("Getting Frame - {}", buffer, ":hex")
|
||||
return buffer
|
||||
return b""
|
||||
|
||||
def populateResult(self, result):
|
||||
"""Populate the modbus result header.
|
||||
|
||||
The serial packets do not have any header information
|
||||
that is copied.
|
||||
|
||||
:param result: The response packet
|
||||
"""
|
||||
result.slave_id = self._header["uid"]
|
||||
result.transaction_id = self._header["tid"]
|
||||
|
||||
def getFrameStart(self, slaves, broadcast, skip_cur_frame):
|
||||
"""Scan buffer for a relevant frame start."""
|
||||
start = 1 if skip_cur_frame else 0
|
||||
if (buf_len := len(self._buffer)) < 4:
|
||||
return False
|
||||
for i in range(start, buf_len - 3): # <slave id><function code><crc 2 bytes>
|
||||
if not broadcast and self._buffer[i] not in slaves:
|
||||
continue
|
||||
if (
|
||||
self._buffer[i + 1] not in self.function_codes
|
||||
and (self._buffer[i + 1] - 0x80) not in self.function_codes
|
||||
):
|
||||
continue
|
||||
if i:
|
||||
self._buffer = self._buffer[i:] # remove preceding trash.
|
||||
return True
|
||||
if buf_len > 3:
|
||||
self._buffer = self._buffer[-3:]
|
||||
return False
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Public Member Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def frameProcessIncomingPacket(self, single, callback, slave, _tid=None, **kwargs):
|
||||
"""Process new packet pattern."""
|
||||
broadcast = not slave[0]
|
||||
skip_cur_frame = False
|
||||
while self.getFrameStart(slave, broadcast, skip_cur_frame):
|
||||
if not self.isFrameReady():
|
||||
Log.debug("Frame - not ready")
|
||||
break
|
||||
if not self.checkFrame():
|
||||
Log.debug("Frame check failed, ignoring!!")
|
||||
self.resetFrame()
|
||||
skip_cur_frame = True
|
||||
continue
|
||||
if not self._validate_slave_id(slave, single):
|
||||
header_txt = self._header["uid"]
|
||||
Log.debug("Not a valid slave id - {}, ignoring!!", header_txt)
|
||||
self.resetFrame()
|
||||
skip_cur_frame = True
|
||||
continue
|
||||
self._process(callback)
|
||||
|
||||
def buildPacket(self, message):
|
||||
"""Create a ready to send modbus packet.
|
||||
|
||||
:param message: The populated request/response to send
|
||||
"""
|
||||
data = message.encode()
|
||||
packet = (
|
||||
struct.pack(RTU_FRAME_HEADER, message.slave_id, message.function_code)
|
||||
+ data
|
||||
)
|
||||
packet += struct.pack(">H", computeCRC(packet))
|
||||
# Ensure that transaction is actually the slave id for serial comms
|
||||
message.transaction_id = message.slave_id
|
||||
return packet
|
||||
|
||||
def sendPacket(self, message):
|
||||
"""Send packets on the bus with 3.5char delay between frames.
|
||||
|
||||
:param message: Message to be sent over the bus
|
||||
:return:
|
||||
"""
|
||||
start = time.time()
|
||||
timeout = start + self.client.comm_params.timeout_connect
|
||||
while self.client.state != ModbusTransactionState.IDLE:
|
||||
if self.client.state == ModbusTransactionState.TRANSACTION_COMPLETE:
|
||||
timestamp = round(time.time(), 6)
|
||||
Log.debug(
|
||||
"Changing state to IDLE - Last Frame End - {} Current Time stamp - {}",
|
||||
self.client.last_frame_end,
|
||||
timestamp,
|
||||
)
|
||||
if self.client.last_frame_end:
|
||||
idle_time = self.client.idle_time()
|
||||
if round(timestamp - idle_time, 6) <= self.client.silent_interval:
|
||||
Log.debug(
|
||||
"Waiting for 3.5 char before next send - {} ms",
|
||||
self.client.silent_interval * 1000,
|
||||
)
|
||||
time.sleep(self.client.silent_interval)
|
||||
else:
|
||||
# Recovering from last error ??
|
||||
time.sleep(self.client.silent_interval)
|
||||
self.client.state = ModbusTransactionState.IDLE
|
||||
elif self.client.state == ModbusTransactionState.RETRYING:
|
||||
# Simple lets settle down!!!
|
||||
# To check for higher baudrates
|
||||
time.sleep(self.client.comm_params.timeout_connect)
|
||||
break
|
||||
elif time.time() > timeout:
|
||||
Log.debug(
|
||||
"Spent more time than the read time out, "
|
||||
"resetting the transaction to IDLE"
|
||||
)
|
||||
self.client.state = ModbusTransactionState.IDLE
|
||||
else:
|
||||
Log.debug("Sleeping")
|
||||
time.sleep(self.client.silent_interval)
|
||||
size = self.client.send(message)
|
||||
self.client.last_frame_end = round(time.time(), 6)
|
||||
return size
|
||||
|
||||
def recvPacket(self, size):
|
||||
"""Receive packet from the bus with specified len.
|
||||
|
||||
:param size: Number of bytes to read
|
||||
:return:
|
||||
"""
|
||||
result = self.client.recv(size)
|
||||
self.client.last_frame_end = round(time.time(), 6)
|
||||
return result
|
||||
|
||||
def _process(self, callback, error=False):
|
||||
"""Process incoming packets irrespective error condition."""
|
||||
data = self._buffer if error else self.getFrame()
|
||||
if (result := self.decoder.decode(data)) is None:
|
||||
raise ModbusIOException("Unable to decode request")
|
||||
if error and result.function_code < 0x80:
|
||||
raise InvalidMessageReceivedException(result)
|
||||
self.populateResult(result)
|
||||
self.advanceFrame()
|
||||
callback(result) # defer or push to a thread?
|
||||
|
||||
def get_expected_response_length(self, data):
|
||||
"""Get the expected response length.
|
||||
|
||||
:param data: Message data read so far
|
||||
:raises IndexError: If not enough data to read byte count
|
||||
:return: Total frame size
|
||||
"""
|
||||
func_code = int(data[1])
|
||||
pdu_class = self.decoder.lookupPduClass(func_code)
|
||||
return pdu_class.calculateRtuFrameSize(data)
|
||||
|
||||
|
||||
# __END__
|
||||
182
env/lib/python3.11/site-packages/pymodbus/framer/socket_framer.py
vendored
Normal file
182
env/lib/python3.11/site-packages/pymodbus/framer/socket_framer.py
vendored
Normal file
@@ -0,0 +1,182 @@
|
||||
"""Socket framer."""
|
||||
# pylint: disable=missing-type-doc
|
||||
import struct
|
||||
|
||||
from pymodbus.exceptions import (
|
||||
InvalidMessageReceivedException,
|
||||
ModbusIOException,
|
||||
)
|
||||
from pymodbus.framer.base import SOCKET_FRAME_HEADER, ModbusFramer
|
||||
from pymodbus.logging import Log
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus TCP Message
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class ModbusSocketFramer(ModbusFramer):
|
||||
"""Modbus Socket Frame controller.
|
||||
|
||||
Before each modbus TCP message is an MBAP header which is used as a
|
||||
message frame. It allows us to easily separate messages as follows::
|
||||
|
||||
[ MBAP Header ] [ Function Code] [ Data ] \
|
||||
[ tid ][ pid ][ length ][ uid ]
|
||||
2b 2b 2b 1b 1b Nb
|
||||
|
||||
while len(message) > 0:
|
||||
tid, pid, length`, uid = struct.unpack(">HHHB", message)
|
||||
request = message[0:7 + length - 1`]
|
||||
message = [7 + length - 1:]
|
||||
|
||||
* length = uid + function code + data
|
||||
* The -1 is to account for the uid byte
|
||||
"""
|
||||
|
||||
method = "socket"
|
||||
|
||||
def __init__(self, decoder, client=None):
|
||||
"""Initialize a new instance of the framer.
|
||||
|
||||
:param decoder: The decoder factory implementation to use
|
||||
"""
|
||||
super().__init__(decoder, client)
|
||||
self._hsize = 0x07
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Private Helper Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def checkFrame(self):
|
||||
"""Check and decode the next frame.
|
||||
|
||||
Return true if we were successful.
|
||||
"""
|
||||
if self.isFrameReady():
|
||||
(
|
||||
self._header["tid"],
|
||||
self._header["pid"],
|
||||
self._header["len"],
|
||||
self._header["uid"],
|
||||
) = struct.unpack(">HHHB", self._buffer[0 : self._hsize])
|
||||
|
||||
# someone sent us an error? ignore it
|
||||
if self._header["len"] < 2:
|
||||
self.advanceFrame()
|
||||
# we have at least a complete message, continue
|
||||
elif len(self._buffer) - self._hsize + 1 >= self._header["len"]:
|
||||
return True
|
||||
# we don't have enough of a message yet, wait
|
||||
return False
|
||||
|
||||
def advanceFrame(self):
|
||||
"""Skip over the current framed message.
|
||||
|
||||
This allows us to skip over the current message after we have processed
|
||||
it or determined that it contains an error. It also has to reset the
|
||||
current frame header handle
|
||||
"""
|
||||
length = self._hsize + self._header["len"] - 1
|
||||
self._buffer = self._buffer[length:]
|
||||
self._header = {"tid": 0, "pid": 0, "len": 0, "uid": 0}
|
||||
|
||||
def isFrameReady(self):
|
||||
"""Check if we should continue decode logic.
|
||||
|
||||
This is meant to be used in a while loop in the decoding phase to let
|
||||
the decoder factory know that there is still data in the buffer.
|
||||
|
||||
:returns: True if ready, False otherwise
|
||||
"""
|
||||
return len(self._buffer) > self._hsize
|
||||
|
||||
def getFrame(self):
|
||||
"""Return the next frame from the buffered data.
|
||||
|
||||
:returns: The next full frame buffer
|
||||
"""
|
||||
length = self._hsize + self._header["len"] - 1
|
||||
return self._buffer[self._hsize : length]
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Public Member Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def decode_data(self, data):
|
||||
"""Decode data."""
|
||||
if len(data) > self._hsize:
|
||||
tid, pid, length, uid, fcode = struct.unpack(
|
||||
SOCKET_FRAME_HEADER, data[0 : self._hsize + 1]
|
||||
)
|
||||
return {
|
||||
"tid": tid,
|
||||
"pid": pid,
|
||||
"length": length,
|
||||
"slave": uid,
|
||||
"fcode": fcode,
|
||||
}
|
||||
return {}
|
||||
|
||||
def frameProcessIncomingPacket(self, single, callback, slave, tid=None, **kwargs):
|
||||
"""Process new packet pattern.
|
||||
|
||||
This takes in a new request packet, adds it to the current
|
||||
packet stream, and performs framing on it. That is, checks
|
||||
for complete messages, and once found, will process all that
|
||||
exist. This handles the case when we read N + 1 or 1 // N
|
||||
messages at a time instead of 1.
|
||||
|
||||
The processed and decoded messages are pushed to the callback
|
||||
function to process and send.
|
||||
"""
|
||||
while True:
|
||||
if not self.isFrameReady():
|
||||
if len(self._buffer):
|
||||
# Possible error ???
|
||||
if self._header["len"] < 2:
|
||||
self._process(callback, tid, error=True)
|
||||
break
|
||||
if not self.checkFrame():
|
||||
Log.debug("Frame check failed, ignoring!!")
|
||||
self.resetFrame()
|
||||
continue
|
||||
if not self._validate_slave_id(slave, single):
|
||||
header_txt = self._header["uid"]
|
||||
Log.debug("Not a valid slave id - {}, ignoring!!", header_txt)
|
||||
self.resetFrame()
|
||||
continue
|
||||
self._process(callback, tid)
|
||||
|
||||
def _process(self, callback, tid, error=False):
|
||||
"""Process incoming packets irrespective error condition."""
|
||||
data = self._buffer if error else self.getFrame()
|
||||
if (result := self.decoder.decode(data)) is None:
|
||||
self.resetFrame()
|
||||
raise ModbusIOException("Unable to decode request")
|
||||
if error and result.function_code < 0x80:
|
||||
raise InvalidMessageReceivedException(result)
|
||||
self.populateResult(result)
|
||||
self.advanceFrame()
|
||||
if tid and tid != result.transaction_id:
|
||||
self.resetFrame()
|
||||
else:
|
||||
callback(result) # defer or push to a thread?
|
||||
|
||||
def buildPacket(self, message):
|
||||
"""Create a ready to send modbus packet.
|
||||
|
||||
:param message: The populated request/response to send
|
||||
"""
|
||||
data = message.encode()
|
||||
packet = struct.pack(
|
||||
SOCKET_FRAME_HEADER,
|
||||
message.transaction_id,
|
||||
message.protocol_id,
|
||||
len(data) + 2,
|
||||
message.slave_id,
|
||||
message.function_code,
|
||||
)
|
||||
packet += data
|
||||
return packet
|
||||
|
||||
|
||||
# __END__
|
||||
127
env/lib/python3.11/site-packages/pymodbus/framer/tls_framer.py
vendored
Normal file
127
env/lib/python3.11/site-packages/pymodbus/framer/tls_framer.py
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
"""TLS framer."""
|
||||
# pylint: disable=missing-type-doc
|
||||
import struct
|
||||
|
||||
from pymodbus.exceptions import (
|
||||
InvalidMessageReceivedException,
|
||||
ModbusIOException,
|
||||
)
|
||||
from pymodbus.framer.base import TLS_FRAME_HEADER, ModbusFramer
|
||||
from pymodbus.logging import Log
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus TLS Message
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class ModbusTlsFramer(ModbusFramer):
|
||||
"""Modbus TLS Frame controller
|
||||
|
||||
No prefix MBAP header before decrypted PDU is used as a message frame for
|
||||
Modbus Security Application Protocol. It allows us to easily separate
|
||||
decrypted messages which is PDU as follows:
|
||||
|
||||
[ Function Code] [ Data ]
|
||||
1b Nb
|
||||
"""
|
||||
|
||||
method = "tls"
|
||||
|
||||
def __init__(self, decoder, client=None):
|
||||
"""Initialize a new instance of the framer.
|
||||
|
||||
:param decoder: The decoder factory implementation to use
|
||||
"""
|
||||
super().__init__(decoder, client)
|
||||
self._hsize = 0x0
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Private Helper Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def checkFrame(self):
|
||||
"""Check and decode the next frame.
|
||||
|
||||
Return true if we were successful.
|
||||
"""
|
||||
if self.isFrameReady():
|
||||
# we have at least a complete message, continue
|
||||
if len(self._buffer) - self._hsize >= 1:
|
||||
return True
|
||||
# we don't have enough of a message yet, wait
|
||||
return False
|
||||
|
||||
def advanceFrame(self):
|
||||
"""Skip over the current framed message.
|
||||
|
||||
This allows us to skip over the current message after we have processed
|
||||
it or determined that it contains an error. It also has to reset the
|
||||
current frame header handle
|
||||
"""
|
||||
self._buffer = b""
|
||||
self._header = {}
|
||||
|
||||
def isFrameReady(self):
|
||||
"""Check if we should continue decode logic.
|
||||
|
||||
This is meant to be used in a while loop in the decoding phase to let
|
||||
the decoder factory know that there is still data in the buffer.
|
||||
|
||||
:returns: True if ready, False otherwise
|
||||
"""
|
||||
return len(self._buffer) > self._hsize
|
||||
|
||||
def getFrame(self):
|
||||
"""Return the next frame from the buffered data.
|
||||
|
||||
:returns: The next full frame buffer
|
||||
"""
|
||||
return self._buffer[self._hsize :]
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Public Member Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def decode_data(self, data):
|
||||
"""Decode data."""
|
||||
if len(data) > self._hsize:
|
||||
(fcode,) = struct.unpack(TLS_FRAME_HEADER, data[0 : self._hsize + 1])
|
||||
return {"fcode": fcode}
|
||||
return {}
|
||||
|
||||
def frameProcessIncomingPacket(self, single, callback, slave, _tid=None, **kwargs):
|
||||
"""Process new packet pattern."""
|
||||
# no slave id for Modbus Security Application Protocol
|
||||
if not self.isFrameReady():
|
||||
return
|
||||
if not self.checkFrame():
|
||||
Log.debug("Frame check failed, ignoring!!")
|
||||
self.resetFrame()
|
||||
return
|
||||
if not self._validate_slave_id(slave, single):
|
||||
Log.debug("Not in valid slave id - {}, ignoring!!", slave)
|
||||
self.resetFrame()
|
||||
self._process(callback)
|
||||
|
||||
def _process(self, callback, error=False):
|
||||
"""Process incoming packets irrespective error condition."""
|
||||
data = self._buffer if error else self.getFrame()
|
||||
if (result := self.decoder.decode(data)) is None:
|
||||
raise ModbusIOException("Unable to decode request")
|
||||
if error and result.function_code < 0x80:
|
||||
raise InvalidMessageReceivedException(result)
|
||||
self.populateResult(result)
|
||||
self.advanceFrame()
|
||||
callback(result) # defer or push to a thread?
|
||||
|
||||
def buildPacket(self, message):
|
||||
"""Create a ready to send modbus packet.
|
||||
|
||||
:param message: The populated request/response to send
|
||||
"""
|
||||
data = message.encode()
|
||||
packet = struct.pack(TLS_FRAME_HEADER, message.function_code)
|
||||
packet += data
|
||||
return packet
|
||||
|
||||
|
||||
# __END__
|
||||
120
env/lib/python3.11/site-packages/pymodbus/logging.py
vendored
Normal file
120
env/lib/python3.11/site-packages/pymodbus/logging.py
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
"""Pymodbus: Modbus Protocol Implementation.
|
||||
|
||||
Released under the the BSD license
|
||||
"""
|
||||
import logging
|
||||
from binascii import b2a_hex
|
||||
from logging import NullHandler as __null
|
||||
from typing import Union
|
||||
|
||||
from pymodbus.utilities import hexlify_packets
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Block unhandled logging
|
||||
# ---------------------------------------------------------------------------#
|
||||
logging.getLogger("pymodbus_internal").addHandler(__null())
|
||||
|
||||
|
||||
def pymodbus_apply_logging_config(
|
||||
level: Union[str, int] = logging.DEBUG, log_file_name: str = None
|
||||
):
|
||||
"""Apply basic logging configuration used by default by Pymodbus maintainers.
|
||||
|
||||
:param level: (optional) set log level, if not set it is inherited.
|
||||
:param log_file_name: (optional) log additional to file
|
||||
|
||||
Please call this function to format logging appropriately when opening issues.
|
||||
"""
|
||||
if isinstance(level, str):
|
||||
level = level.upper()
|
||||
Log.apply_logging_config(level, log_file_name)
|
||||
|
||||
|
||||
class Log:
|
||||
"""Class to hide logging complexity.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@classmethod
|
||||
def apply_logging_config(cls, level, log_file_name):
|
||||
"""Apply basic logging configuration"""
|
||||
if level == logging.NOTSET:
|
||||
level = cls._logger.getEffectiveLevel()
|
||||
if isinstance(level, str):
|
||||
level = level.upper()
|
||||
log_stream_handler = logging.StreamHandler()
|
||||
log_formatter = logging.Formatter(
|
||||
"%(asctime)s %(levelname)-5s %(module)s:%(lineno)s %(message)s"
|
||||
)
|
||||
log_stream_handler.setFormatter(log_formatter)
|
||||
cls._logger.addHandler(log_stream_handler)
|
||||
if log_file_name:
|
||||
log_file_handler = logging.FileHandler(log_file_name)
|
||||
log_file_handler.setFormatter(log_formatter)
|
||||
cls._logger.addHandler(log_file_handler)
|
||||
cls.setLevel(level)
|
||||
|
||||
@classmethod
|
||||
def setLevel(cls, level):
|
||||
"""Apply basic logging level"""
|
||||
cls._logger.setLevel(level)
|
||||
|
||||
@classmethod
|
||||
def build_msg(cls, txt, *args):
|
||||
"""Build message."""
|
||||
string_args = []
|
||||
count_args = len(args) - 1
|
||||
skip = False
|
||||
for i in range(count_args + 1):
|
||||
if skip:
|
||||
skip = False
|
||||
continue
|
||||
if (
|
||||
i < count_args
|
||||
and isinstance(args[i + 1], str)
|
||||
and args[i + 1][0] == ":"
|
||||
):
|
||||
if args[i + 1] == ":hex":
|
||||
string_args.append(hexlify_packets(args[i]))
|
||||
elif args[i + 1] == ":str":
|
||||
string_args.append(str(args[i]))
|
||||
elif args[i + 1] == ":b2a":
|
||||
string_args.append(b2a_hex(args[i]))
|
||||
skip = True
|
||||
else:
|
||||
string_args.append(args[i])
|
||||
return txt.format(*string_args)
|
||||
|
||||
@classmethod
|
||||
def info(cls, txt, *args):
|
||||
"""Log info messagees."""
|
||||
if cls._logger.isEnabledFor(logging.INFO):
|
||||
cls._logger.info(cls.build_msg(txt, *args))
|
||||
|
||||
@classmethod
|
||||
def debug(cls, txt, *args):
|
||||
"""Log debug messagees."""
|
||||
if cls._logger.isEnabledFor(logging.DEBUG):
|
||||
cls._logger.debug(cls.build_msg(txt, *args))
|
||||
|
||||
@classmethod
|
||||
def warning(cls, txt, *args):
|
||||
"""Log warning messagees."""
|
||||
if cls._logger.isEnabledFor(logging.WARNING):
|
||||
cls._logger.warning(cls.build_msg(txt, *args))
|
||||
|
||||
@classmethod
|
||||
def error(cls, txt, *args):
|
||||
"""Log error messagees."""
|
||||
if cls._logger.isEnabledFor(logging.ERROR):
|
||||
cls._logger.error(cls.build_msg(txt, *args))
|
||||
|
||||
@classmethod
|
||||
def critical(cls, txt, *args):
|
||||
"""Log critical messagees."""
|
||||
if cls._logger.isEnabledFor(logging.CRITICAL):
|
||||
cls._logger.critical(cls.build_msg(txt, *args))
|
||||
221
env/lib/python3.11/site-packages/pymodbus/mei_message.py
vendored
Normal file
221
env/lib/python3.11/site-packages/pymodbus/mei_message.py
vendored
Normal file
@@ -0,0 +1,221 @@
|
||||
"""Encapsulated Interface (MEI) Transport Messages."""
|
||||
|
||||
__all__ = [
|
||||
"ReadDeviceInformationRequest",
|
||||
"ReadDeviceInformationResponse",
|
||||
]
|
||||
|
||||
# pylint: disable=missing-type-doc
|
||||
import struct
|
||||
|
||||
from pymodbus.constants import DeviceInformation, MoreData
|
||||
from pymodbus.device import DeviceInformationFactory, ModbusControlBlock
|
||||
from pymodbus.pdu import ModbusExceptions as merror
|
||||
from pymodbus.pdu import ModbusRequest, ModbusResponse
|
||||
|
||||
|
||||
_MCB = ModbusControlBlock()
|
||||
|
||||
|
||||
class _OutOfSpaceException(Exception):
|
||||
"""Internal out of space exception."""
|
||||
|
||||
# This exception exists here as a simple, local way to manage response
|
||||
# length control for the only MODBUS command which requires it under
|
||||
# standard, non-error conditions. It and the structures associated with
|
||||
# it should ideally be refactored and applied to all responses, however,
|
||||
# since a Client can make requests which result in disallowed conditions,
|
||||
# such as, for instance, requesting a register read of more registers
|
||||
# than will fit in a single PDU. As per the specification, the PDU is
|
||||
# restricted to 253 bytes, irrespective of the transport used.
|
||||
#
|
||||
# See Page 5/50 of MODBUS Application Protocol Specification V1.1b3.
|
||||
|
||||
def __init__(self, oid):
|
||||
self.oid = oid
|
||||
super().__init__()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Read Device Information
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ReadDeviceInformationRequest(ModbusRequest):
|
||||
"""Read device information.
|
||||
|
||||
This function code allows reading the identification and additional
|
||||
information relative to the physical and functional description of a
|
||||
remote device, only.
|
||||
|
||||
The Read Device Identification interface is modeled as an address space
|
||||
composed of a set of addressable data elements. The data elements are
|
||||
called objects and an object Id identifies them.
|
||||
"""
|
||||
|
||||
function_code = 0x2B
|
||||
sub_function_code = 0x0E
|
||||
function_code_name = "read_device_information"
|
||||
_rtu_frame_size = 7
|
||||
|
||||
def __init__(self, read_code=None, object_id=0x00, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param read_code: The device information read code
|
||||
:param object_id: The object to read from
|
||||
"""
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.read_code = read_code or DeviceInformation.BASIC
|
||||
self.object_id = object_id
|
||||
|
||||
def encode(self):
|
||||
"""Encode the request packet.
|
||||
|
||||
:returns: The byte encoded packet
|
||||
"""
|
||||
packet = struct.pack(
|
||||
">BBB", self.sub_function_code, self.read_code, self.object_id
|
||||
)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode data part of the message.
|
||||
|
||||
:param data: The incoming data
|
||||
"""
|
||||
params = struct.unpack(">BBB", data)
|
||||
self.sub_function_code, self.read_code, self.object_id = params
|
||||
|
||||
def execute(self, _context):
|
||||
"""Run a read exception status request against the store.
|
||||
|
||||
:returns: The populated response
|
||||
"""
|
||||
if not 0x00 <= self.object_id <= 0xFF:
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not 0x00 <= self.read_code <= 0x04:
|
||||
return self.doException(merror.IllegalValue)
|
||||
|
||||
information = DeviceInformationFactory.get(_MCB, self.read_code, self.object_id)
|
||||
return ReadDeviceInformationResponse(self.read_code, information)
|
||||
|
||||
def __str__(self):
|
||||
"""Build a representation of the request.
|
||||
|
||||
:returns: The string representation of the request
|
||||
"""
|
||||
params = (self.read_code, self.object_id)
|
||||
return (
|
||||
"ReadDeviceInformationRequest(%d,%d)" # pylint: disable=consider-using-f-string
|
||||
% params
|
||||
)
|
||||
|
||||
|
||||
class ReadDeviceInformationResponse(ModbusResponse):
|
||||
"""Read device information response."""
|
||||
|
||||
function_code = 0x2B
|
||||
sub_function_code = 0x0E
|
||||
|
||||
@classmethod
|
||||
def calculateRtuFrameSize(cls, buffer):
|
||||
"""Calculate the size of the message
|
||||
|
||||
:param buffer: A buffer containing the data that have been received.
|
||||
:returns: The number of bytes in the response.
|
||||
"""
|
||||
size = 8 # skip the header information
|
||||
count = int(buffer[7])
|
||||
|
||||
try:
|
||||
while count > 0:
|
||||
_, object_length = struct.unpack(">BB", buffer[size : size + 2])
|
||||
size += object_length + 2
|
||||
count -= 1
|
||||
return size + 2
|
||||
except struct.error as exc:
|
||||
raise IndexError from exc
|
||||
|
||||
def __init__(self, read_code=None, information=None, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param read_code: The device information read code
|
||||
:param information: The requested information request
|
||||
"""
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.read_code = read_code or DeviceInformation.BASIC
|
||||
self.information = information or {}
|
||||
self.number_of_objects = 0
|
||||
self.conformity = 0x83 # I support everything right now
|
||||
self.next_object_id = 0x00
|
||||
self.more_follows = MoreData.NOTHING
|
||||
self.space_left = None
|
||||
|
||||
def _encode_object(self, object_id, data):
|
||||
"""Encode object."""
|
||||
self.space_left -= 2 + len(data)
|
||||
if self.space_left <= 0:
|
||||
raise _OutOfSpaceException(object_id)
|
||||
encoded_obj = struct.pack(">BB", object_id, len(data))
|
||||
if isinstance(data, bytes):
|
||||
encoded_obj += data
|
||||
else:
|
||||
encoded_obj += data.encode()
|
||||
self.number_of_objects += 1
|
||||
return encoded_obj
|
||||
|
||||
def encode(self):
|
||||
"""Encode the response.
|
||||
|
||||
:returns: The byte encoded message
|
||||
"""
|
||||
packet = struct.pack(
|
||||
">BBB", self.sub_function_code, self.read_code, self.conformity
|
||||
)
|
||||
self.space_left = 253 - 6
|
||||
objects = b""
|
||||
try:
|
||||
for object_id, data in iter(self.information.items()):
|
||||
if isinstance(data, list):
|
||||
for item in data:
|
||||
objects += self._encode_object(object_id, item)
|
||||
else:
|
||||
objects += self._encode_object(object_id, data)
|
||||
except _OutOfSpaceException as exc:
|
||||
self.next_object_id = exc.oid
|
||||
self.more_follows = MoreData.KEEP_READING
|
||||
|
||||
packet += struct.pack(
|
||||
">BBB", self.more_follows, self.next_object_id, self.number_of_objects
|
||||
)
|
||||
packet += objects
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode a the response.
|
||||
|
||||
:param data: The packet data to decode
|
||||
"""
|
||||
params = struct.unpack(">BBBBBB", data[0:6])
|
||||
self.sub_function_code, self.read_code = params[0:2]
|
||||
self.conformity, self.more_follows = params[2:4]
|
||||
self.next_object_id, self.number_of_objects = params[4:6]
|
||||
self.information, count = {}, 6 # skip the header information
|
||||
|
||||
while count < len(data):
|
||||
object_id, object_length = struct.unpack(">BB", data[count : count + 2])
|
||||
count += object_length + 2
|
||||
if object_id not in self.information:
|
||||
self.information[object_id] = data[count - object_length : count]
|
||||
elif isinstance(self.information[object_id], list):
|
||||
self.information[object_id].append(data[count - object_length : count])
|
||||
else:
|
||||
self.information[object_id] = [
|
||||
self.information[object_id],
|
||||
data[count - object_length : count],
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
"""Build a representation of the response.
|
||||
|
||||
:returns: The string representation of the response
|
||||
"""
|
||||
return f"ReadDeviceInformationResponse({self.read_code})"
|
||||
483
env/lib/python3.11/site-packages/pymodbus/other_message.py
vendored
Normal file
483
env/lib/python3.11/site-packages/pymodbus/other_message.py
vendored
Normal file
@@ -0,0 +1,483 @@
|
||||
"""Diagnostic record read/write.
|
||||
|
||||
Currently not all implemented
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
"ReadExceptionStatusRequest",
|
||||
"ReadExceptionStatusResponse",
|
||||
"GetCommEventCounterRequest",
|
||||
"GetCommEventCounterResponse",
|
||||
"GetCommEventLogRequest",
|
||||
"GetCommEventLogResponse",
|
||||
"ReportSlaveIdRequest",
|
||||
"ReportSlaveIdResponse",
|
||||
]
|
||||
|
||||
# pylint: disable=missing-type-doc
|
||||
import struct
|
||||
|
||||
from pymodbus.constants import ModbusStatus
|
||||
from pymodbus.device import DeviceInformationFactory, ModbusControlBlock
|
||||
from pymodbus.pdu import ModbusRequest, ModbusResponse
|
||||
|
||||
|
||||
_MCB = ModbusControlBlock()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# TODO Make these only work on serial # pylint: disable=fixme
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ReadExceptionStatusRequest(ModbusRequest):
|
||||
"""This function code is used to read the contents of eight Exception Status outputs in a remote device.
|
||||
|
||||
The function provides a simple method for
|
||||
accessing this information, because the Exception Output references are
|
||||
known (no output reference is needed in the function).
|
||||
"""
|
||||
|
||||
function_code = 0x07
|
||||
function_code_name = "read_exception_status"
|
||||
_rtu_frame_size = 4
|
||||
|
||||
def __init__(self, slave=None, **kwargs):
|
||||
"""Initialize a new instance."""
|
||||
ModbusRequest.__init__(self, slave=slave, **kwargs)
|
||||
|
||||
def encode(self):
|
||||
"""Encode the message."""
|
||||
return b""
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode data part of the message.
|
||||
|
||||
:param data: The incoming data
|
||||
"""
|
||||
|
||||
def execute(self, _context=None):
|
||||
"""Run a read exception status request against the store.
|
||||
|
||||
:returns: The populated response
|
||||
"""
|
||||
status = _MCB.Counter.summary()
|
||||
return ReadExceptionStatusResponse(status)
|
||||
|
||||
def __str__(self):
|
||||
"""Build a representation of the request.
|
||||
|
||||
:returns: The string representation of the request
|
||||
"""
|
||||
return f"ReadExceptionStatusRequest({self.function_code})"
|
||||
|
||||
|
||||
class ReadExceptionStatusResponse(ModbusResponse):
|
||||
"""The normal response contains the status of the eight Exception Status outputs.
|
||||
|
||||
The outputs are packed into one data byte, with one bit
|
||||
per output. The status of the lowest output reference is contained
|
||||
in the least significant bit of the byte. The contents of the eight
|
||||
Exception Status outputs are device specific.
|
||||
"""
|
||||
|
||||
function_code = 0x07
|
||||
_rtu_frame_size = 5
|
||||
|
||||
def __init__(self, status=0x00, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param status: The status response to report
|
||||
"""
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.status = status if status < 256 else 255
|
||||
|
||||
def encode(self):
|
||||
"""Encode the response.
|
||||
|
||||
:returns: The byte encoded message
|
||||
"""
|
||||
return struct.pack(">B", self.status)
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode a the response.
|
||||
|
||||
:param data: The packet data to decode
|
||||
"""
|
||||
self.status = int(data[0])
|
||||
|
||||
def __str__(self):
|
||||
"""Build a representation of the response.
|
||||
|
||||
:returns: The string representation of the response
|
||||
"""
|
||||
arguments = (self.function_code, self.status)
|
||||
return (
|
||||
"ReadExceptionStatusResponse(%d, %s)" # pylint: disable=consider-using-f-string
|
||||
% arguments
|
||||
)
|
||||
|
||||
|
||||
# Encapsulate interface transport 43, 14
|
||||
# CANopen general reference 43, 13
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# TODO Make these only work on serial # pylint: disable=fixme
|
||||
# ---------------------------------------------------------------------------#
|
||||
class GetCommEventCounterRequest(ModbusRequest):
|
||||
"""This function code is used to get a status word.
|
||||
|
||||
And an event count from the remote device's communication event counter.
|
||||
|
||||
By fetching the current count before and after a series of messages, a
|
||||
client can determine whether the messages were handled normally by the
|
||||
remote device.
|
||||
|
||||
The device's event counter is incremented once for each successful
|
||||
message completion. It is not incremented for exception responses,
|
||||
poll commands, or fetch event counter commands.
|
||||
|
||||
The event counter can be reset by means of the Diagnostics function
|
||||
(code 08), with a subfunction of Restart Communications Option
|
||||
(code 00 01) or Clear Counters and Diagnostic Register (code 00 0A).
|
||||
"""
|
||||
|
||||
function_code = 0x0B
|
||||
function_code_name = "get_event_counter"
|
||||
_rtu_frame_size = 4
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize a new instance."""
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
|
||||
def encode(self):
|
||||
"""Encode the message."""
|
||||
return b""
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode data part of the message.
|
||||
|
||||
:param data: The incoming data
|
||||
"""
|
||||
|
||||
def execute(self, _context=None):
|
||||
"""Run a read exception status request against the store.
|
||||
|
||||
:returns: The populated response
|
||||
"""
|
||||
status = _MCB.Counter.Event
|
||||
return GetCommEventCounterResponse(status)
|
||||
|
||||
def __str__(self):
|
||||
"""Build a representation of the request.
|
||||
|
||||
:returns: The string representation of the request
|
||||
"""
|
||||
return f"GetCommEventCounterRequest({self.function_code})"
|
||||
|
||||
|
||||
class GetCommEventCounterResponse(ModbusResponse):
|
||||
"""Get comm event counter response.
|
||||
|
||||
The normal response contains a two-byte status word, and a two-byte
|
||||
event count. The status word will be all ones (FF FF hex) if a
|
||||
previously-issued program command is still being processed by the
|
||||
remote device (a busy condition exists). Otherwise, the status word
|
||||
will be all zeros.
|
||||
"""
|
||||
|
||||
function_code = 0x0B
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, count=0x0000, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param count: The current event counter value
|
||||
"""
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.count = count
|
||||
self.status = True # this means we are ready, not waiting
|
||||
|
||||
def encode(self):
|
||||
"""Encode the response.
|
||||
|
||||
:returns: The byte encoded message
|
||||
"""
|
||||
if self.status:
|
||||
ready = ModbusStatus.READY
|
||||
else:
|
||||
ready = ModbusStatus.WAITING
|
||||
return struct.pack(">HH", ready, self.count)
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode a the response.
|
||||
|
||||
:param data: The packet data to decode
|
||||
"""
|
||||
ready, self.count = struct.unpack(">HH", data)
|
||||
self.status = ready == ModbusStatus.READY
|
||||
|
||||
def __str__(self):
|
||||
"""Build a representation of the response.
|
||||
|
||||
:returns: The string representation of the response
|
||||
"""
|
||||
arguments = (self.function_code, self.count, self.status)
|
||||
return (
|
||||
"GetCommEventCounterResponse(%d, %d, %d)" # pylint: disable=consider-using-f-string
|
||||
% arguments
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# TODO Make these only work on serial # pylint: disable=fixme
|
||||
# ---------------------------------------------------------------------------#
|
||||
class GetCommEventLogRequest(ModbusRequest):
|
||||
"""This function code is used to get a status word.
|
||||
|
||||
Event count, message count, and a field of event bytes from the remote device.
|
||||
|
||||
The status word and event counts are identical to that returned by
|
||||
the Get Communications Event Counter function (11, 0B hex).
|
||||
|
||||
The message counter contains the quantity of messages processed by the
|
||||
remote device since its last restart, clear counters operation, or
|
||||
power-up. This count is identical to that returned by the Diagnostic
|
||||
function (code 08), sub-function Return Bus Message Count (code 11,
|
||||
0B hex).
|
||||
|
||||
The event bytes field contains 0-64 bytes, with each byte corresponding
|
||||
to the status of one MODBUS send or receive operation for the remote
|
||||
device. The remote device enters the events into the field in
|
||||
chronological order. Byte 0 is the most recent event. Each new byte
|
||||
flushes the oldest byte from the field.
|
||||
"""
|
||||
|
||||
function_code = 0x0C
|
||||
function_code_name = "get_event_log"
|
||||
_rtu_frame_size = 4
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize a new instance."""
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
|
||||
def encode(self):
|
||||
"""Encode the message."""
|
||||
return b""
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode data part of the message.
|
||||
|
||||
:param data: The incoming data
|
||||
"""
|
||||
|
||||
def execute(self, _context=None):
|
||||
"""Run a read exception status request against the store.
|
||||
|
||||
:returns: The populated response
|
||||
"""
|
||||
results = {
|
||||
"status": True,
|
||||
"message_count": _MCB.Counter.BusMessage,
|
||||
"event_count": _MCB.Counter.Event,
|
||||
"events": _MCB.getEvents(),
|
||||
}
|
||||
return GetCommEventLogResponse(**results)
|
||||
|
||||
def __str__(self):
|
||||
"""Build a representation of the request.
|
||||
|
||||
:returns: The string representation of the request
|
||||
"""
|
||||
return f"GetCommEventLogRequest({self.function_code})"
|
||||
|
||||
|
||||
class GetCommEventLogResponse(ModbusResponse):
|
||||
"""Get Comm event log response.
|
||||
|
||||
The normal response contains a two-byte status word field,
|
||||
a two-byte event count field, a two-byte message count field,
|
||||
and a field containing 0-64 bytes of events. A byte count
|
||||
field defines the total length of the data in these four field
|
||||
"""
|
||||
|
||||
function_code = 0x0C
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param status: The status response to report
|
||||
:param message_count: The current message count
|
||||
:param event_count: The current event count
|
||||
:param events: The collection of events to send
|
||||
"""
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.status = kwargs.get("status", True)
|
||||
self.message_count = kwargs.get("message_count", 0)
|
||||
self.event_count = kwargs.get("event_count", 0)
|
||||
self.events = kwargs.get("events", [])
|
||||
|
||||
def encode(self):
|
||||
"""Encode the response.
|
||||
|
||||
:returns: The byte encoded message
|
||||
"""
|
||||
if self.status:
|
||||
ready = ModbusStatus.READY
|
||||
else:
|
||||
ready = ModbusStatus.WAITING
|
||||
packet = struct.pack(">B", 6 + len(self.events))
|
||||
packet += struct.pack(">H", ready)
|
||||
packet += struct.pack(">HH", self.event_count, self.message_count)
|
||||
packet += b"".join(struct.pack(">B", e) for e in self.events)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode a the response.
|
||||
|
||||
:param data: The packet data to decode
|
||||
"""
|
||||
length = int(data[0])
|
||||
status = struct.unpack(">H", data[1:3])[0]
|
||||
self.status = status == ModbusStatus.READY
|
||||
self.event_count = struct.unpack(">H", data[3:5])[0]
|
||||
self.message_count = struct.unpack(">H", data[5:7])[0]
|
||||
|
||||
self.events = []
|
||||
for i in range(7, length + 1):
|
||||
self.events.append(int(data[i]))
|
||||
|
||||
def __str__(self):
|
||||
"""Build a representation of the response.
|
||||
|
||||
:returns: The string representation of the response
|
||||
"""
|
||||
arguments = (
|
||||
self.function_code,
|
||||
self.status,
|
||||
self.message_count,
|
||||
self.event_count,
|
||||
)
|
||||
return (
|
||||
"GetCommEventLogResponse(%d, %d, %d, %d)" # pylint: disable=consider-using-f-string
|
||||
% arguments
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# TODO Make these only work on serial # pylint: disable=fixme
|
||||
# ---------------------------------------------------------------------------#
|
||||
class ReportSlaveIdRequest(ModbusRequest):
|
||||
"""This function code is used to read the description of the type.
|
||||
|
||||
The current status, and other information specific to a remote device.
|
||||
"""
|
||||
|
||||
function_code = 0x11
|
||||
function_code_name = "report_slave_id"
|
||||
_rtu_frame_size = 4
|
||||
|
||||
def __init__(self, slave=0, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param slave: Modbus slave slave ID
|
||||
|
||||
"""
|
||||
ModbusRequest.__init__(self, slave, **kwargs)
|
||||
|
||||
def encode(self):
|
||||
"""Encode the message."""
|
||||
return b""
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode data part of the message.
|
||||
|
||||
:param data: The incoming data
|
||||
"""
|
||||
|
||||
def execute(self, context=None):
|
||||
"""Run a report slave id request against the store.
|
||||
|
||||
:returns: The populated response
|
||||
"""
|
||||
report_slave_id_data = None
|
||||
if context:
|
||||
report_slave_id_data = getattr(context, "reportSlaveIdData", None)
|
||||
if not report_slave_id_data:
|
||||
information = DeviceInformationFactory.get(_MCB)
|
||||
|
||||
# Support identity values as bytes data and regular str data
|
||||
id_data = []
|
||||
for v_item in information.values():
|
||||
if isinstance(v_item, bytes):
|
||||
id_data.append(v_item)
|
||||
else:
|
||||
id_data.append(v_item.encode())
|
||||
|
||||
identifier = b"-".join(id_data)
|
||||
identifier = identifier or b"Pymodbus"
|
||||
report_slave_id_data = identifier
|
||||
return ReportSlaveIdResponse(report_slave_id_data)
|
||||
|
||||
def __str__(self):
|
||||
"""Build a representation of the request.
|
||||
|
||||
:returns: The string representation of the request
|
||||
"""
|
||||
return f"ReportSlaveIdRequest({self.function_code})"
|
||||
|
||||
|
||||
class ReportSlaveIdResponse(ModbusResponse):
|
||||
"""Show response.
|
||||
|
||||
The data contents are specific to each type of device.
|
||||
"""
|
||||
|
||||
function_code = 0x11
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, identifier=b"\x00", status=True, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param identifier: The identifier of the slave
|
||||
:param status: The status response to report
|
||||
"""
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.identifier = identifier
|
||||
self.status = status
|
||||
self.byte_count = None
|
||||
|
||||
def encode(self):
|
||||
"""Encode the response.
|
||||
|
||||
:returns: The byte encoded message
|
||||
"""
|
||||
if self.status:
|
||||
status = ModbusStatus.SLAVE_ON
|
||||
else:
|
||||
status = ModbusStatus.SLAVE_OFF
|
||||
length = len(self.identifier) + 1
|
||||
packet = struct.pack(">B", length)
|
||||
packet += self.identifier # we assume it is already encoded
|
||||
packet += struct.pack(">B", status)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode a the response.
|
||||
|
||||
Since the identifier is device dependent, we just return the
|
||||
raw value that a user can decode to whatever it should be.
|
||||
|
||||
:param data: The packet data to decode
|
||||
"""
|
||||
self.byte_count = int(data[0])
|
||||
self.identifier = data[1 : self.byte_count + 1]
|
||||
status = int(data[-1])
|
||||
self.status = status == ModbusStatus.SLAVE_ON
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Build a representation of the response.
|
||||
|
||||
:returns: The string representation of the response
|
||||
"""
|
||||
return f"ReportSlaveIdResponse({self.function_code}, {self.identifier}, {self.status})"
|
||||
460
env/lib/python3.11/site-packages/pymodbus/payload.py
vendored
Normal file
460
env/lib/python3.11/site-packages/pymodbus/payload.py
vendored
Normal file
@@ -0,0 +1,460 @@
|
||||
"""Modbus Payload Builders.
|
||||
|
||||
A collection of utilities for building and decoding
|
||||
modbus messages payloads.
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
"BinaryPayloadBuilder",
|
||||
"BinaryPayloadDecoder",
|
||||
]
|
||||
|
||||
# pylint: disable=missing-type-doc
|
||||
from struct import pack, unpack
|
||||
from typing import List
|
||||
|
||||
from pymodbus.constants import Endian
|
||||
from pymodbus.exceptions import ParameterException
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.utilities import (
|
||||
pack_bitstring,
|
||||
unpack_bitstring,
|
||||
)
|
||||
|
||||
|
||||
WC = {"b": 1, "h": 2, "e": 2, "i": 4, "l": 4, "q": 8, "f": 4, "d": 8}
|
||||
|
||||
|
||||
class BinaryPayloadBuilder:
|
||||
"""A utility that helps build payload messages to be written with the various modbus messages.
|
||||
|
||||
It really is just a simple wrapper around the struct module,
|
||||
however it saves time looking up the format strings.
|
||||
What follows is a simple example::
|
||||
|
||||
builder = BinaryPayloadBuilder(byteorder=Endian.Little)
|
||||
builder.add_8bit_uint(1)
|
||||
builder.add_16bit_uint(2)
|
||||
payload = builder.build()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, payload=None, byteorder=Endian.LITTLE, wordorder=Endian.BIG, repack=False
|
||||
):
|
||||
"""Initialize a new instance of the payload builder.
|
||||
|
||||
:param payload: Raw binary payload data to initialize with
|
||||
:param byteorder: The endianness of the bytes in the words
|
||||
:param wordorder: The endianness of the word (when wordcount is >= 2)
|
||||
:param repack: Repack the provided payload based on BO
|
||||
"""
|
||||
self._payload = payload or []
|
||||
self._byteorder = byteorder
|
||||
self._wordorder = wordorder
|
||||
self._repack = repack
|
||||
|
||||
def _pack_words(self, fstring, value):
|
||||
"""Pack words based on the word order and byte order.
|
||||
|
||||
# ---------------------------------------------- #
|
||||
# pack in to network ordered value #
|
||||
# unpack in to network ordered unsigned integer #
|
||||
# Change Word order if little endian word order #
|
||||
# Pack values back based on correct byte order #
|
||||
# ---------------------------------------------- #
|
||||
|
||||
:param fstring:
|
||||
:param value: Value to be packed
|
||||
:return:
|
||||
"""
|
||||
value = pack(f"!{fstring}", value)
|
||||
wordorder = WC.get(fstring.lower()) // 2
|
||||
upperbyte = f"!{wordorder}H"
|
||||
payload = unpack(upperbyte, value)
|
||||
|
||||
if self._wordorder == Endian.LITTLE:
|
||||
payload = list(reversed(payload))
|
||||
|
||||
fstring = self._byteorder + "H"
|
||||
payload = [pack(fstring, word) for word in payload]
|
||||
payload = b"".join(payload)
|
||||
|
||||
return payload
|
||||
|
||||
def encode(self) -> bytes:
|
||||
"""Get the payload buffer encoded in bytes."""
|
||||
return b"".join(self._payload)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return the payload buffer as a string.
|
||||
|
||||
:returns: The payload buffer as a string
|
||||
"""
|
||||
return self.encode().decode("utf-8")
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset the payload buffer."""
|
||||
self._payload = []
|
||||
|
||||
def to_registers(self):
|
||||
"""Convert the payload buffer to register layout that can be used as a context block.
|
||||
|
||||
:returns: The register layout to use as a block
|
||||
"""
|
||||
# fstring = self._byteorder+"H"
|
||||
fstring = "!H"
|
||||
payload = self.build()
|
||||
if self._repack:
|
||||
payload = [unpack(self._byteorder + "H", value)[0] for value in payload]
|
||||
else:
|
||||
payload = [unpack(fstring, value)[0] for value in payload]
|
||||
Log.debug("{}", payload)
|
||||
return payload
|
||||
|
||||
def to_coils(self) -> List[bool]:
|
||||
"""Convert the payload buffer into a coil layout that can be used as a context block.
|
||||
|
||||
:returns: The coil layout to use as a block
|
||||
"""
|
||||
payload = self.to_registers()
|
||||
coils = [bool(int(bit)) for reg in payload for bit in format(reg, "016b")]
|
||||
return coils
|
||||
|
||||
def build(self) -> List[bytes]:
|
||||
"""Return the payload buffer as a list.
|
||||
|
||||
This list is two bytes per element and can
|
||||
thus be treated as a list of registers.
|
||||
|
||||
:returns: The payload buffer as a list
|
||||
"""
|
||||
buffer = self.encode()
|
||||
length = len(buffer)
|
||||
buffer += b"\x00" * (length % 2)
|
||||
return [buffer[i : i + 2] for i in range(0, length, 2)]
|
||||
|
||||
def add_bits(self, values: List[bool]) -> None:
|
||||
"""Add a collection of bits to be encoded.
|
||||
|
||||
If these are less than a multiple of eight,
|
||||
they will be left padded with 0 bits to make
|
||||
it so.
|
||||
|
||||
:param values: The value to add to the buffer
|
||||
"""
|
||||
value = pack_bitstring(values)
|
||||
self._payload.append(value)
|
||||
|
||||
def add_8bit_uint(self, value: int) -> None:
|
||||
"""Add a 8 bit unsigned int to the buffer.
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = self._byteorder + "B"
|
||||
self._payload.append(pack(fstring, value))
|
||||
|
||||
def add_16bit_uint(self, value: int) -> None:
|
||||
"""Add a 16 bit unsigned int to the buffer.
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = self._byteorder + "H"
|
||||
self._payload.append(pack(fstring, value))
|
||||
|
||||
def add_32bit_uint(self, value: int) -> None:
|
||||
"""Add a 32 bit unsigned int to the buffer.
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = "I"
|
||||
# fstring = self._byteorder + "I"
|
||||
p_string = self._pack_words(fstring, value)
|
||||
self._payload.append(p_string)
|
||||
|
||||
def add_64bit_uint(self, value: int) -> None:
|
||||
"""Add a 64 bit unsigned int to the buffer.
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = "Q"
|
||||
p_string = self._pack_words(fstring, value)
|
||||
self._payload.append(p_string)
|
||||
|
||||
def add_8bit_int(self, value: int) -> None:
|
||||
"""Add a 8 bit signed int to the buffer.
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = self._byteorder + "b"
|
||||
self._payload.append(pack(fstring, value))
|
||||
|
||||
def add_16bit_int(self, value: int) -> None:
|
||||
"""Add a 16 bit signed int to the buffer.
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = self._byteorder + "h"
|
||||
self._payload.append(pack(fstring, value))
|
||||
|
||||
def add_32bit_int(self, value: int) -> None:
|
||||
"""Add a 32 bit signed int to the buffer.
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = "i"
|
||||
p_string = self._pack_words(fstring, value)
|
||||
self._payload.append(p_string)
|
||||
|
||||
def add_64bit_int(self, value: int) -> None:
|
||||
"""Add a 64 bit signed int to the buffer.
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = "q"
|
||||
p_string = self._pack_words(fstring, value)
|
||||
self._payload.append(p_string)
|
||||
|
||||
def add_16bit_float(self, value: float) -> None:
|
||||
"""Add a 16 bit float to the buffer.
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = "e"
|
||||
p_string = self._pack_words(fstring, value)
|
||||
self._payload.append(p_string)
|
||||
|
||||
def add_32bit_float(self, value: float) -> None:
|
||||
"""Add a 32 bit float to the buffer.
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = "f"
|
||||
p_string = self._pack_words(fstring, value)
|
||||
self._payload.append(p_string)
|
||||
|
||||
def add_64bit_float(self, value: float) -> None:
|
||||
"""Add a 64 bit float(double) to the buffer.
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = "d"
|
||||
p_string = self._pack_words(fstring, value)
|
||||
self._payload.append(p_string)
|
||||
|
||||
def add_string(self, value: str) -> None:
|
||||
"""Add a string to the buffer.
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = self._byteorder + str(len(value)) + "s"
|
||||
self._payload.append(pack(fstring, value.encode()))
|
||||
|
||||
|
||||
class BinaryPayloadDecoder:
|
||||
"""A utility that helps decode payload messages from a modbus response message.
|
||||
|
||||
It really is just a simple wrapper around
|
||||
the struct module, however it saves time looking up the format
|
||||
strings. What follows is a simple example::
|
||||
|
||||
decoder = BinaryPayloadDecoder(payload)
|
||||
first = decoder.decode_8bit_uint()
|
||||
second = decoder.decode_16bit_uint()
|
||||
"""
|
||||
|
||||
def __init__(self, payload, byteorder=Endian.LITTLE, wordorder=Endian.BIG):
|
||||
"""Initialize a new payload decoder.
|
||||
|
||||
:param payload: The payload to decode with
|
||||
:param byteorder: The endianness of the payload
|
||||
:param wordorder: The endianness of the word (when wordcount is >= 2)
|
||||
"""
|
||||
self._payload = payload
|
||||
self._pointer = 0x00
|
||||
self._byteorder = byteorder
|
||||
self._wordorder = wordorder
|
||||
|
||||
@classmethod
|
||||
def fromRegisters(
|
||||
cls,
|
||||
registers,
|
||||
byteorder=Endian.LITTLE,
|
||||
wordorder=Endian.BIG,
|
||||
):
|
||||
"""Initialize a payload decoder.
|
||||
|
||||
With the result of reading a collection of registers from a modbus device.
|
||||
|
||||
The registers are treated as a list of 2 byte values.
|
||||
We have to do this because of how the data has already
|
||||
been decoded by the rest of the library.
|
||||
|
||||
:param registers: The register results to initialize with
|
||||
:param byteorder: The Byte order of each word
|
||||
:param wordorder: The endianness of the word (when wordcount is >= 2)
|
||||
:returns: An initialized PayloadDecoder
|
||||
:raises ParameterException:
|
||||
"""
|
||||
Log.debug("{}", registers)
|
||||
if isinstance(registers, list): # repack into flat binary
|
||||
payload = b"".join(pack("!H", x) for x in registers)
|
||||
return cls(payload, byteorder, wordorder)
|
||||
raise ParameterException("Invalid collection of registers supplied")
|
||||
|
||||
@classmethod
|
||||
def bit_chunks(cls, coils, size=8):
|
||||
"""Return bit chunks."""
|
||||
chunks = [coils[i : i + size] for i in range(0, len(coils), size)]
|
||||
return chunks
|
||||
|
||||
@classmethod
|
||||
def fromCoils(
|
||||
cls,
|
||||
coils,
|
||||
byteorder=Endian.LITTLE,
|
||||
_wordorder=Endian.BIG,
|
||||
):
|
||||
"""Initialize a payload decoder with the result of reading of coils."""
|
||||
if isinstance(coils, list):
|
||||
payload = b""
|
||||
if padding := len(coils) % 8: # Pad zeros
|
||||
extra = [False] * padding
|
||||
coils = extra + coils
|
||||
chunks = cls.bit_chunks(coils)
|
||||
for chunk in chunks:
|
||||
payload += pack_bitstring(chunk[::-1])
|
||||
return cls(payload, byteorder)
|
||||
raise ParameterException("Invalid collection of coils supplied")
|
||||
|
||||
def _unpack_words(self, fstring, handle):
|
||||
"""Unpack words based on the word order and byte order.
|
||||
|
||||
# ---------------------------------------------- #
|
||||
# Unpack in to network ordered unsigned integer #
|
||||
# Change Word order if little endian word order #
|
||||
# Pack values back based on correct byte order #
|
||||
# ---------------------------------------------- #
|
||||
:param fstring:
|
||||
:param handle: Value to be unpacked
|
||||
:return:
|
||||
"""
|
||||
wc_value = WC.get(fstring.lower()) // 2
|
||||
handle = unpack(f"!{wc_value}H", handle)
|
||||
if self._wordorder == Endian.LITTLE:
|
||||
handle = list(reversed(handle))
|
||||
|
||||
# Repack as unsigned Integer
|
||||
handle = [pack(self._byteorder + "H", p) for p in handle]
|
||||
Log.debug("handle: {}", handle)
|
||||
handle = b"".join(handle)
|
||||
return handle
|
||||
|
||||
def reset(self):
|
||||
"""Reset the decoder pointer back to the start."""
|
||||
self._pointer = 0x00
|
||||
|
||||
def decode_8bit_uint(self):
|
||||
"""Decode a 8 bit unsigned int from the buffer."""
|
||||
self._pointer += 1
|
||||
fstring = self._byteorder + "B"
|
||||
handle = self._payload[self._pointer - 1 : self._pointer]
|
||||
return unpack(fstring, handle)[0]
|
||||
|
||||
def decode_bits(self, package_len=1):
|
||||
"""Decode a byte worth of bits from the buffer."""
|
||||
self._pointer += package_len
|
||||
# fstring = self._endian + "B"
|
||||
handle = self._payload[self._pointer - 1 : self._pointer]
|
||||
return unpack_bitstring(handle)
|
||||
|
||||
def decode_16bit_uint(self):
|
||||
"""Decode a 16 bit unsigned int from the buffer."""
|
||||
self._pointer += 2
|
||||
fstring = self._byteorder + "H"
|
||||
handle = self._payload[self._pointer - 2 : self._pointer]
|
||||
return unpack(fstring, handle)[0]
|
||||
|
||||
def decode_32bit_uint(self):
|
||||
"""Decode a 32 bit unsigned int from the buffer."""
|
||||
self._pointer += 4
|
||||
fstring = "I"
|
||||
handle = self._payload[self._pointer - 4 : self._pointer]
|
||||
handle = self._unpack_words(fstring, handle)
|
||||
return unpack("!" + fstring, handle)[0]
|
||||
|
||||
def decode_64bit_uint(self):
|
||||
"""Decode a 64 bit unsigned int from the buffer."""
|
||||
self._pointer += 8
|
||||
fstring = "Q"
|
||||
handle = self._payload[self._pointer - 8 : self._pointer]
|
||||
handle = self._unpack_words(fstring, handle)
|
||||
return unpack("!" + fstring, handle)[0]
|
||||
|
||||
def decode_8bit_int(self):
|
||||
"""Decode a 8 bit signed int from the buffer."""
|
||||
self._pointer += 1
|
||||
fstring = self._byteorder + "b"
|
||||
handle = self._payload[self._pointer - 1 : self._pointer]
|
||||
return unpack(fstring, handle)[0]
|
||||
|
||||
def decode_16bit_int(self):
|
||||
"""Decode a 16 bit signed int from the buffer."""
|
||||
self._pointer += 2
|
||||
fstring = self._byteorder + "h"
|
||||
handle = self._payload[self._pointer - 2 : self._pointer]
|
||||
return unpack(fstring, handle)[0]
|
||||
|
||||
def decode_32bit_int(self):
|
||||
"""Decode a 32 bit signed int from the buffer."""
|
||||
self._pointer += 4
|
||||
fstring = "i"
|
||||
handle = self._payload[self._pointer - 4 : self._pointer]
|
||||
handle = self._unpack_words(fstring, handle)
|
||||
return unpack("!" + fstring, handle)[0]
|
||||
|
||||
def decode_64bit_int(self):
|
||||
"""Decode a 64 bit signed int from the buffer."""
|
||||
self._pointer += 8
|
||||
fstring = "q"
|
||||
handle = self._payload[self._pointer - 8 : self._pointer]
|
||||
handle = self._unpack_words(fstring, handle)
|
||||
return unpack("!" + fstring, handle)[0]
|
||||
|
||||
def decode_16bit_float(self):
|
||||
"""Decode a 16 bit float from the buffer."""
|
||||
self._pointer += 2
|
||||
fstring = "e"
|
||||
handle = self._payload[self._pointer - 2 : self._pointer]
|
||||
handle = self._unpack_words(fstring, handle)
|
||||
return unpack("!" + fstring, handle)[0]
|
||||
|
||||
def decode_32bit_float(self):
|
||||
"""Decode a 32 bit float from the buffer."""
|
||||
self._pointer += 4
|
||||
fstring = "f"
|
||||
handle = self._payload[self._pointer - 4 : self._pointer]
|
||||
handle = self._unpack_words(fstring, handle)
|
||||
return unpack("!" + fstring, handle)[0]
|
||||
|
||||
def decode_64bit_float(self):
|
||||
"""Decode a 64 bit float(double) from the buffer."""
|
||||
self._pointer += 8
|
||||
fstring = "d"
|
||||
handle = self._payload[self._pointer - 8 : self._pointer]
|
||||
handle = self._unpack_words(fstring, handle)
|
||||
return unpack("!" + fstring, handle)[0]
|
||||
|
||||
def decode_string(self, size=1):
|
||||
"""Decode a string from the buffer.
|
||||
|
||||
:param size: The size of the string to decode
|
||||
"""
|
||||
self._pointer += size
|
||||
return self._payload[self._pointer - size : self._pointer]
|
||||
|
||||
def skip_bytes(self, nbytes):
|
||||
"""Skip n bytes in the buffer.
|
||||
|
||||
:param nbytes: The number of bytes to skip
|
||||
"""
|
||||
self._pointer += nbytes
|
||||
256
env/lib/python3.11/site-packages/pymodbus/pdu.py
vendored
Normal file
256
env/lib/python3.11/site-packages/pymodbus/pdu.py
vendored
Normal file
@@ -0,0 +1,256 @@
|
||||
"""Contains base classes for modbus request/response/error packets."""
|
||||
|
||||
__all__ = [
|
||||
"ModbusRequest",
|
||||
"ModbusResponse",
|
||||
"ModbusExceptions",
|
||||
"ExceptionResponse",
|
||||
"IllegalFunctionRequest",
|
||||
]
|
||||
|
||||
# pylint: disable=missing-type-doc
|
||||
import struct
|
||||
|
||||
from pymodbus.exceptions import NotImplementedException
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.utilities import rtuFrameSize
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Base PDUs
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusPDU:
|
||||
"""Base class for all Modbus messages.
|
||||
|
||||
.. attribute:: transaction_id
|
||||
|
||||
This value is used to uniquely identify a request
|
||||
response pair. It can be implemented as a simple counter
|
||||
|
||||
.. attribute:: protocol_id
|
||||
|
||||
This is a constant set at 0 to indicate Modbus. It is
|
||||
put here for ease of expansion.
|
||||
|
||||
.. attribute:: slave_id
|
||||
|
||||
This is used to route the request to the correct child. In
|
||||
the TCP modbus, it is used for routing (or not used at all. However,
|
||||
for the serial versions, it is used to specify which child to perform
|
||||
the requests against. The value 0x00 represents the broadcast address
|
||||
(also 0xff).
|
||||
|
||||
.. attribute:: check
|
||||
|
||||
This is used for LRC/CRC in the serial modbus protocols
|
||||
|
||||
.. attribute:: skip_encode
|
||||
|
||||
This is used when the message payload has already been encoded.
|
||||
Generally this will occur when the PayloadBuilder is being used
|
||||
to create a complicated message. By setting this to True, the
|
||||
request will pass the currently encoded message through instead
|
||||
of encoding it again.
|
||||
"""
|
||||
|
||||
def __init__(self, slave=0, **kwargs):
|
||||
"""Initialize the base data for a modbus request.
|
||||
|
||||
:param slave: Modbus slave slave ID
|
||||
|
||||
"""
|
||||
self.transaction_id = kwargs.get("transaction", 0)
|
||||
self.protocol_id = kwargs.get("protocol", 0)
|
||||
self.slave_id = slave
|
||||
self.skip_encode = kwargs.get("skip_encode", False)
|
||||
self.check = 0x0000
|
||||
|
||||
def encode(self):
|
||||
"""Encode the message.
|
||||
|
||||
:raises: A not implemented exception
|
||||
"""
|
||||
raise NotImplementedException()
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode data part of the message.
|
||||
|
||||
:param data: is a string object
|
||||
:raises NotImplementedException:
|
||||
"""
|
||||
raise NotImplementedException()
|
||||
|
||||
@classmethod
|
||||
def calculateRtuFrameSize(cls, buffer):
|
||||
"""Calculate the size of a PDU.
|
||||
|
||||
:param buffer: A buffer containing the data that have been received.
|
||||
:returns: The number of bytes in the PDU.
|
||||
:raises NotImplementedException:
|
||||
"""
|
||||
if hasattr(cls, "_rtu_frame_size"):
|
||||
return cls._rtu_frame_size
|
||||
if hasattr(cls, "_rtu_byte_count_pos"):
|
||||
return rtuFrameSize(buffer, cls._rtu_byte_count_pos)
|
||||
raise NotImplementedException(
|
||||
f"Cannot determine RTU frame size for {cls.__name__}"
|
||||
)
|
||||
|
||||
|
||||
class ModbusRequest(ModbusPDU):
|
||||
"""Base class for a modbus request PDU."""
|
||||
|
||||
function_code = -1
|
||||
|
||||
def __init__(self, slave=0, **kwargs): # pylint: disable=useless-parent-delegation
|
||||
"""Proxy to the lower level initializer.
|
||||
|
||||
:param slave: Modbus slave slave ID
|
||||
"""
|
||||
super().__init__(slave, **kwargs)
|
||||
|
||||
def doException(self, exception):
|
||||
"""Build an error response based on the function.
|
||||
|
||||
:param exception: The exception to return
|
||||
:raises: An exception response
|
||||
"""
|
||||
exc = ExceptionResponse(self.function_code, exception)
|
||||
Log.error("Exception response {}", exc)
|
||||
return exc
|
||||
|
||||
|
||||
class ModbusResponse(ModbusPDU):
|
||||
"""Base class for a modbus response PDU.
|
||||
|
||||
.. attribute:: should_respond
|
||||
|
||||
A flag that indicates if this response returns a result back
|
||||
to the client issuing the request
|
||||
|
||||
.. attribute:: _rtu_frame_size
|
||||
|
||||
Indicates the size of the modbus rtu response used for
|
||||
calculating how much to read.
|
||||
"""
|
||||
|
||||
should_respond = True
|
||||
function_code = 0x00
|
||||
|
||||
def __init__(self, slave=0, **kwargs):
|
||||
"""Proxy the lower level initializer.
|
||||
|
||||
:param slave: Modbus slave slave ID
|
||||
|
||||
"""
|
||||
super().__init__(slave, **kwargs)
|
||||
self.bits = []
|
||||
self.registers = []
|
||||
|
||||
def isError(self) -> bool:
|
||||
"""Check if the error is a success or failure."""
|
||||
return self.function_code > 0x80
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exception PDUs
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusExceptions: # pylint: disable=too-few-public-methods
|
||||
"""An enumeration of the valid modbus exceptions."""
|
||||
|
||||
IllegalFunction = 0x01
|
||||
IllegalAddress = 0x02
|
||||
IllegalValue = 0x03
|
||||
SlaveFailure = 0x04
|
||||
Acknowledge = 0x05
|
||||
SlaveBusy = 0x06
|
||||
MemoryParityError = 0x08
|
||||
GatewayPathUnavailable = 0x0A
|
||||
GatewayNoResponse = 0x0B
|
||||
|
||||
@classmethod
|
||||
def decode(cls, code):
|
||||
"""Give an error code, translate it to a string error name.
|
||||
|
||||
:param code: The code number to translate
|
||||
"""
|
||||
values = {
|
||||
v: k
|
||||
for k, v in iter(cls.__dict__.items())
|
||||
if not k.startswith("__") and not callable(v)
|
||||
}
|
||||
return values.get(code, None)
|
||||
|
||||
|
||||
class ExceptionResponse(ModbusResponse):
|
||||
"""Base class for a modbus exception PDU."""
|
||||
|
||||
ExceptionOffset = 0x80
|
||||
_rtu_frame_size = 5
|
||||
|
||||
def __init__(self, function_code, exception_code=None, **kwargs):
|
||||
"""Initialize the modbus exception response.
|
||||
|
||||
:param function_code: The function to build an exception response for
|
||||
:param exception_code: The specific modbus exception to return
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self.original_code = function_code
|
||||
self.function_code = function_code | self.ExceptionOffset
|
||||
self.exception_code = exception_code
|
||||
|
||||
def encode(self):
|
||||
"""Encode a modbus exception response.
|
||||
|
||||
:returns: The encoded exception packet
|
||||
"""
|
||||
return struct.pack(">B", self.exception_code)
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode a modbus exception response.
|
||||
|
||||
:param data: The packet data to decode
|
||||
"""
|
||||
self.exception_code = int(data[0])
|
||||
|
||||
def __str__(self):
|
||||
"""Build a representation of an exception response.
|
||||
|
||||
:returns: The string representation of an exception response
|
||||
"""
|
||||
message = ModbusExceptions.decode(self.exception_code)
|
||||
parameters = (self.function_code, self.original_code, message)
|
||||
return (
|
||||
"Exception Response(%d, %d, %s)" # pylint: disable=consider-using-f-string
|
||||
% parameters
|
||||
)
|
||||
|
||||
|
||||
class IllegalFunctionRequest(ModbusRequest):
|
||||
"""Define the Modbus slave exception type "Illegal Function".
|
||||
|
||||
This exception code is returned if the slave::
|
||||
|
||||
- does not implement the function code **or**
|
||||
- is not in a state that allows it to process the function
|
||||
"""
|
||||
|
||||
ErrorCode = 1
|
||||
|
||||
def __init__(self, function_code, **kwargs):
|
||||
"""Initialize a IllegalFunctionRequest.
|
||||
|
||||
:param function_code: The function we are erroring on
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self.function_code = function_code
|
||||
|
||||
def decode(self, _data):
|
||||
"""Decode so this failure will run correctly."""
|
||||
|
||||
def execute(self, _context):
|
||||
"""Build an illegal function request error response.
|
||||
|
||||
:returns: The error response packet
|
||||
"""
|
||||
return ExceptionResponse(self.function_code, self.ErrorCode)
|
||||
0
env/lib/python3.11/site-packages/pymodbus/py.typed
vendored
Normal file
0
env/lib/python3.11/site-packages/pymodbus/py.typed
vendored
Normal file
411
env/lib/python3.11/site-packages/pymodbus/register_read_message.py
vendored
Normal file
411
env/lib/python3.11/site-packages/pymodbus/register_read_message.py
vendored
Normal file
@@ -0,0 +1,411 @@
|
||||
"""Register Reading Request/Response."""
|
||||
|
||||
__all__ = [
|
||||
"ReadHoldingRegistersRequest",
|
||||
"ReadHoldingRegistersResponse",
|
||||
"ReadInputRegistersRequest",
|
||||
"ReadInputRegistersResponse",
|
||||
"ReadRegistersResponseBase",
|
||||
"ReadWriteMultipleRegistersRequest",
|
||||
"ReadWriteMultipleRegistersResponse",
|
||||
]
|
||||
|
||||
# pylint: disable=missing-type-doc
|
||||
import struct
|
||||
|
||||
from pymodbus.pdu import ExceptionResponse, ModbusRequest, ModbusResponse
|
||||
from pymodbus.pdu import ModbusExceptions as merror
|
||||
|
||||
|
||||
class ReadRegistersRequestBase(ModbusRequest):
|
||||
"""Base class for reading a modbus register."""
|
||||
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address, count, slave=0, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param address: The address to start the read from
|
||||
:param count: The number of registers to read
|
||||
:param slave: Modbus slave slave ID
|
||||
"""
|
||||
super().__init__(slave, **kwargs)
|
||||
self.address = address
|
||||
self.count = count
|
||||
|
||||
def encode(self):
|
||||
"""Encode the request packet.
|
||||
|
||||
:return: The encoded packet
|
||||
"""
|
||||
return struct.pack(">HH", self.address, self.count)
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode a register request packet.
|
||||
|
||||
:param data: The request to decode
|
||||
"""
|
||||
self.address, self.count = struct.unpack(">HH", data)
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""Get response pdu size.
|
||||
|
||||
Func_code (1 byte) + Byte Count(1 byte) + 2 * Quantity of Coils (n Bytes).
|
||||
"""
|
||||
return 1 + 1 + 2 * self.count
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of the instance.
|
||||
|
||||
:returns: A string representation of the instance
|
||||
"""
|
||||
return f"{self.__class__.__name__} ({self.address},{self.count})"
|
||||
|
||||
|
||||
class ReadRegistersResponseBase(ModbusResponse):
|
||||
"""Base class for responding to a modbus register read.
|
||||
|
||||
The requested registers can be found in the .registers list.
|
||||
"""
|
||||
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, values, slave=0, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param values: The values to write to
|
||||
:param slave: Modbus slave slave ID
|
||||
"""
|
||||
super().__init__(slave, **kwargs)
|
||||
|
||||
#: A list of register values
|
||||
self.registers = values or []
|
||||
|
||||
def encode(self):
|
||||
"""Encode the response packet.
|
||||
|
||||
:returns: The encoded packet
|
||||
"""
|
||||
result = struct.pack(">B", len(self.registers) * 2)
|
||||
for register in self.registers:
|
||||
result += struct.pack(">H", register)
|
||||
return result
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode a register response packet.
|
||||
|
||||
:param data: The request to decode
|
||||
"""
|
||||
byte_count = int(data[0])
|
||||
self.registers = []
|
||||
for i in range(1, byte_count + 1, 2):
|
||||
self.registers.append(struct.unpack(">H", data[i : i + 2])[0])
|
||||
|
||||
def getRegister(self, index):
|
||||
"""Get the requested register.
|
||||
|
||||
:param index: The indexed register to retrieve
|
||||
:returns: The request register
|
||||
"""
|
||||
return self.registers[index]
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of the instance.
|
||||
|
||||
:returns: A string representation of the instance
|
||||
"""
|
||||
return f"{self.__class__.__name__} ({len(self.registers)})"
|
||||
|
||||
|
||||
class ReadHoldingRegistersRequest(ReadRegistersRequestBase):
|
||||
"""Read holding registers.
|
||||
|
||||
This function code is used to read the contents of a contiguous block
|
||||
of holding registers in a remote device. The Request PDU specifies the
|
||||
starting register address and the number of registers. In the PDU
|
||||
Registers are addressed starting at zero. Therefore registers numbered
|
||||
1-16 are addressed as 0-15.
|
||||
"""
|
||||
|
||||
function_code = 3
|
||||
function_code_name = "read_holding_registers"
|
||||
|
||||
def __init__(self, address=None, count=None, slave=0, **kwargs):
|
||||
"""Initialize a new instance of the request.
|
||||
|
||||
:param address: The starting address to read from
|
||||
:param count: The number of registers to read from address
|
||||
:param slave: Modbus slave slave ID
|
||||
"""
|
||||
super().__init__(address, count, slave, **kwargs)
|
||||
|
||||
def execute(self, context):
|
||||
"""Run a read holding request against a datastore.
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: An initialized :py:class:`~pymodbus.register_read_message.ReadHoldingRegistersResponse`, or an :py:class:`~pymodbus.pdu.ExceptionResponse` if an error occurred
|
||||
"""
|
||||
if not (1 <= self.count <= 0x7D):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, self.count):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
values = context.getValues(self.function_code, self.address, self.count)
|
||||
if isinstance(values, ExceptionResponse):
|
||||
return values
|
||||
|
||||
return ReadHoldingRegistersResponse(values)
|
||||
|
||||
|
||||
class ReadHoldingRegistersResponse(ReadRegistersResponseBase):
|
||||
"""Read holding registers.
|
||||
|
||||
This function code is used to read the contents of a contiguous block
|
||||
of holding registers in a remote device. The Request PDU specifies the
|
||||
starting register address and the number of registers. In the PDU
|
||||
Registers are addressed starting at zero. Therefore registers numbered
|
||||
1-16 are addressed as 0-15.
|
||||
|
||||
The requested registers can be found in the .registers list.
|
||||
"""
|
||||
|
||||
function_code = 3
|
||||
|
||||
def __init__(self, values=None, **kwargs):
|
||||
"""Initialize a new response instance.
|
||||
|
||||
:param values: The resulting register values
|
||||
"""
|
||||
super().__init__(values, **kwargs)
|
||||
|
||||
|
||||
class ReadInputRegistersRequest(ReadRegistersRequestBase):
|
||||
"""Read input registers.
|
||||
|
||||
This function code is used to read from 1 to approx. 125 contiguous
|
||||
input registers in a remote device. The Request PDU specifies the
|
||||
starting register address and the number of registers. In the PDU
|
||||
Registers are addressed starting at zero. Therefore input registers
|
||||
numbered 1-16 are addressed as 0-15.
|
||||
"""
|
||||
|
||||
function_code = 4
|
||||
function_code_name = "read_input_registers"
|
||||
|
||||
def __init__(self, address=None, count=None, slave=0, **kwargs):
|
||||
"""Initialize a new instance of the request.
|
||||
|
||||
:param address: The starting address to read from
|
||||
:param count: The number of registers to read from address
|
||||
:param slave: Modbus slave slave ID
|
||||
"""
|
||||
super().__init__(address, count, slave, **kwargs)
|
||||
|
||||
def execute(self, context):
|
||||
"""Run a read input request against a datastore.
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: An initialized :py:class:`~pymodbus.register_read_message.ReadInputRegistersResponse`, or an :py:class:`~pymodbus.pdu.ExceptionResponse` if an error occurred
|
||||
"""
|
||||
if not (1 <= self.count <= 0x7D):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, self.count):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
values = context.getValues(self.function_code, self.address, self.count)
|
||||
if isinstance(values, ExceptionResponse):
|
||||
return values
|
||||
return ReadInputRegistersResponse(values)
|
||||
|
||||
|
||||
class ReadInputRegistersResponse(ReadRegistersResponseBase):
|
||||
"""Read/write input registers.
|
||||
|
||||
This function code is used to read from 1 to approx. 125 contiguous
|
||||
input registers in a remote device. The Request PDU specifies the
|
||||
starting register address and the number of registers. In the PDU
|
||||
Registers are addressed starting at zero. Therefore input registers
|
||||
numbered 1-16 are addressed as 0-15.
|
||||
|
||||
The requested registers can be found in the .registers list.
|
||||
"""
|
||||
|
||||
function_code = 4
|
||||
|
||||
def __init__(self, values=None, **kwargs):
|
||||
"""Initialize a new response instance.
|
||||
|
||||
:param values: The resulting register values
|
||||
"""
|
||||
super().__init__(values, **kwargs)
|
||||
|
||||
|
||||
class ReadWriteMultipleRegistersRequest(ModbusRequest):
|
||||
"""Read/write multiple registers.
|
||||
|
||||
This function code performs a combination of one read operation and one
|
||||
write operation in a single MODBUS transaction. The write
|
||||
operation is performed before the read.
|
||||
|
||||
Holding registers are addressed starting at zero. Therefore holding
|
||||
registers 1-16 are addressed in the PDU as 0-15.
|
||||
|
||||
The request specifies the starting address and number of holding
|
||||
registers to be read as well as the starting address, number of holding
|
||||
registers, and the data to be written. The byte count specifies the
|
||||
number of bytes to follow in the write data field."
|
||||
"""
|
||||
|
||||
function_code = 23
|
||||
function_code_name = "read_write_multiple_registers"
|
||||
_rtu_byte_count_pos = 10
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize a new request message.
|
||||
|
||||
:param read_address: The address to start reading from
|
||||
:param read_count: The number of registers to read from address
|
||||
:param write_address: The address to start writing to
|
||||
:param write_registers: The registers to write to the specified address
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self.read_address = kwargs.get("read_address", 0x00)
|
||||
self.read_count = kwargs.get("read_count", 0)
|
||||
self.write_address = kwargs.get("write_address", 0x00)
|
||||
self.write_registers = kwargs.get("write_registers", None)
|
||||
if not hasattr(self.write_registers, "__iter__"):
|
||||
self.write_registers = [self.write_registers]
|
||||
self.write_count = len(self.write_registers)
|
||||
self.write_byte_count = self.write_count * 2
|
||||
|
||||
def encode(self):
|
||||
"""Encode the request packet.
|
||||
|
||||
:returns: The encoded packet
|
||||
"""
|
||||
result = struct.pack(
|
||||
">HHHHB",
|
||||
self.read_address,
|
||||
self.read_count,
|
||||
self.write_address,
|
||||
self.write_count,
|
||||
self.write_byte_count,
|
||||
)
|
||||
for register in self.write_registers:
|
||||
result += struct.pack(">H", register)
|
||||
return result
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode the register request packet.
|
||||
|
||||
:param data: The request to decode
|
||||
"""
|
||||
(
|
||||
self.read_address,
|
||||
self.read_count,
|
||||
self.write_address,
|
||||
self.write_count,
|
||||
self.write_byte_count,
|
||||
) = struct.unpack(">HHHHB", data[:9])
|
||||
self.write_registers = []
|
||||
for i in range(9, self.write_byte_count + 9, 2):
|
||||
register = struct.unpack(">H", data[i : i + 2])[0]
|
||||
self.write_registers.append(register)
|
||||
|
||||
def execute(self, context):
|
||||
"""Run a write single register request against a datastore.
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: An initialized :py:class:`~pymodbus.register_read_message.ReadWriteMultipleRegistersResponse`, or an :py:class:`~pymodbus.pdu.ExceptionResponse` if an error occurred
|
||||
"""
|
||||
if not (1 <= self.read_count <= 0x07D):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not 1 <= self.write_count <= 0x079:
|
||||
return self.doException(merror.IllegalValue)
|
||||
if self.write_byte_count != self.write_count * 2:
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(
|
||||
self.function_code, self.write_address, self.write_count
|
||||
):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
if not context.validate(self.function_code, self.read_address, self.read_count):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
result = context.setValues(
|
||||
self.function_code, self.write_address, self.write_registers
|
||||
)
|
||||
if isinstance(result, ExceptionResponse):
|
||||
return result
|
||||
registers = context.getValues(
|
||||
self.function_code, self.read_address, self.read_count
|
||||
)
|
||||
return ReadWriteMultipleRegistersResponse(registers)
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""Get response pdu size.
|
||||
|
||||
Func_code (1 byte) + Byte Count(1 byte) + 2 * Quantity of Coils (n Bytes)
|
||||
:return:
|
||||
"""
|
||||
return 1 + 1 + 2 * self.read_count
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of the instance.
|
||||
|
||||
:returns: A string representation of the instance
|
||||
"""
|
||||
params = (
|
||||
self.read_address,
|
||||
self.read_count,
|
||||
self.write_address,
|
||||
self.write_count,
|
||||
)
|
||||
return (
|
||||
"ReadWriteNRegisterRequest R(%d,%d) W(%d,%d)" # pylint: disable=consider-using-f-string
|
||||
% params
|
||||
)
|
||||
|
||||
|
||||
class ReadWriteMultipleRegistersResponse(ModbusResponse):
|
||||
"""Read/write multiple registers.
|
||||
|
||||
The normal response contains the data from the group of registers that
|
||||
were read. The byte count field specifies the quantity of bytes to
|
||||
follow in the read data field.
|
||||
|
||||
The requested registers can be found in the .registers list.
|
||||
"""
|
||||
|
||||
function_code = 23
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, values=None, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param values: The register values to write
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self.registers = values or []
|
||||
|
||||
def encode(self):
|
||||
"""Encode the response packet.
|
||||
|
||||
:returns: The encoded packet
|
||||
"""
|
||||
result = struct.pack(">B", len(self.registers) * 2)
|
||||
for register in self.registers:
|
||||
result += struct.pack(">H", register)
|
||||
return result
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode the register response packet.
|
||||
|
||||
:param data: The response to decode
|
||||
"""
|
||||
bytecount = int(data[0])
|
||||
for i in range(1, bytecount, 2):
|
||||
self.registers.append(struct.unpack(">H", data[i : i + 2])[0])
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of the instance.
|
||||
|
||||
:returns: A string representation of the instance
|
||||
"""
|
||||
return f"ReadWriteNRegisterResponse ({len(self.registers)})"
|
||||
379
env/lib/python3.11/site-packages/pymodbus/register_write_message.py
vendored
Normal file
379
env/lib/python3.11/site-packages/pymodbus/register_write_message.py
vendored
Normal file
@@ -0,0 +1,379 @@
|
||||
"""Register Writing Request/Response Messages."""
|
||||
|
||||
__all__ = [
|
||||
"WriteSingleRegisterRequest",
|
||||
"WriteSingleRegisterResponse",
|
||||
"WriteMultipleRegistersRequest",
|
||||
"WriteMultipleRegistersResponse",
|
||||
"MaskWriteRegisterRequest",
|
||||
"MaskWriteRegisterResponse",
|
||||
]
|
||||
|
||||
# pylint: disable=missing-type-doc
|
||||
import struct
|
||||
|
||||
from pymodbus.pdu import ExceptionResponse, ModbusRequest, ModbusResponse
|
||||
from pymodbus.pdu import ModbusExceptions as merror
|
||||
|
||||
|
||||
class WriteSingleRegisterRequest(ModbusRequest):
|
||||
"""This function code is used to write a single holding register in a remote device.
|
||||
|
||||
The Request PDU specifies the address of the register to
|
||||
be written. Registers are addressed starting at zero. Therefore register
|
||||
numbered 1 is addressed as 0.
|
||||
"""
|
||||
|
||||
function_code = 6
|
||||
function_code_name = "write_register"
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address=None, value=None, slave=None, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param address: The address to start writing add
|
||||
:param value: The values to write
|
||||
"""
|
||||
super().__init__(slave=slave, **kwargs)
|
||||
self.address = address
|
||||
self.value = value
|
||||
|
||||
def encode(self):
|
||||
"""Encode a write single register packet packet request.
|
||||
|
||||
:returns: The encoded packet
|
||||
"""
|
||||
packet = struct.pack(">H", self.address)
|
||||
if self.skip_encode:
|
||||
packet += self.value
|
||||
else:
|
||||
packet += struct.pack(">H", self.value)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode a write single register packet packet request.
|
||||
|
||||
:param data: The request to decode
|
||||
"""
|
||||
self.address, self.value = struct.unpack(">HH", data)
|
||||
|
||||
def execute(self, context):
|
||||
"""Run a write single register request against a datastore.
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: An initialized response, exception message otherwise
|
||||
"""
|
||||
if not 0 <= self.value <= 0xFFFF:
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, 1):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
|
||||
result = context.setValues(self.function_code, self.address, [self.value])
|
||||
if isinstance(result, ExceptionResponse):
|
||||
return result
|
||||
values = context.getValues(self.function_code, self.address, 1)
|
||||
return WriteSingleRegisterResponse(self.address, values[0])
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""Get response pdu size.
|
||||
|
||||
Func_code (1 byte) + Register Address(2 byte) + Register Value (2 bytes)
|
||||
:return:
|
||||
"""
|
||||
return 1 + 2 + 2
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of the instance.
|
||||
|
||||
:returns: A string representation of the instance
|
||||
"""
|
||||
return f"WriteRegisterRequest {self.address}"
|
||||
|
||||
|
||||
class WriteSingleRegisterResponse(ModbusResponse):
|
||||
"""The normal response is an echo of the request.
|
||||
|
||||
Returned after the register contents have been written.
|
||||
"""
|
||||
|
||||
function_code = 6
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address=None, value=None, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param address: The address to start writing add
|
||||
:param value: The values to write
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self.address = address
|
||||
self.value = value
|
||||
|
||||
def encode(self):
|
||||
"""Encode a write single register packet packet request.
|
||||
|
||||
:returns: The encoded packet
|
||||
"""
|
||||
return struct.pack(">HH", self.address, self.value)
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode a write single register packet packet request.
|
||||
|
||||
:param data: The request to decode
|
||||
"""
|
||||
self.address, self.value = struct.unpack(">HH", data)
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""Get response pdu size.
|
||||
|
||||
Func_code (1 byte) + Starting Address (2 byte) + And_mask (2 Bytes) + OrMask (2 Bytes)
|
||||
:return:
|
||||
"""
|
||||
return 1 + 2 + 2 + 2
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of the instance.
|
||||
|
||||
:returns: A string representation of the instance
|
||||
"""
|
||||
params = (self.address, self.value)
|
||||
return (
|
||||
"WriteRegisterResponse %d => %d" # pylint: disable=consider-using-f-string
|
||||
% params
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------#
|
||||
# Write Multiple Registers
|
||||
# ---------------------------------------------------------------------------#
|
||||
class WriteMultipleRegistersRequest(ModbusRequest):
|
||||
"""This function code is used to write a block.
|
||||
|
||||
Of contiguous registers (1 to approx. 120 registers) in a remote device.
|
||||
|
||||
The requested written values are specified in the request data field.
|
||||
Data is packed as two bytes per register.
|
||||
"""
|
||||
|
||||
function_code = 16
|
||||
function_code_name = "write_registers"
|
||||
_rtu_byte_count_pos = 6
|
||||
_pdu_length = 5 # func + adress1 + adress2 + outputQuant1 + outputQuant2
|
||||
|
||||
def __init__(self, address=None, values=None, slave=None, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param address: The address to start writing to
|
||||
:param values: The values to write
|
||||
"""
|
||||
super().__init__(slave=slave, **kwargs)
|
||||
self.address = address
|
||||
if values is None:
|
||||
values = []
|
||||
elif not hasattr(values, "__iter__"):
|
||||
values = [values]
|
||||
self.values = values
|
||||
self.count = len(self.values)
|
||||
self.byte_count = self.count * 2
|
||||
|
||||
def encode(self):
|
||||
"""Encode a write single register packet packet request.
|
||||
|
||||
:returns: The encoded packet
|
||||
"""
|
||||
packet = struct.pack(">HHB", self.address, self.count, self.byte_count)
|
||||
if self.skip_encode:
|
||||
return packet + b"".join(self.values)
|
||||
|
||||
for value in self.values:
|
||||
packet += struct.pack(">H", value)
|
||||
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode a write single register packet packet request.
|
||||
|
||||
:param data: The request to decode
|
||||
"""
|
||||
self.address, self.count, self.byte_count = struct.unpack(">HHB", data[:5])
|
||||
self.values = [] # reset
|
||||
for idx in range(5, (self.count * 2) + 5, 2):
|
||||
self.values.append(struct.unpack(">H", data[idx : idx + 2])[0])
|
||||
|
||||
def execute(self, context):
|
||||
"""Run a write single register request against a datastore.
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: An initialized response, exception message otherwise
|
||||
"""
|
||||
if not 1 <= self.count <= 0x07B:
|
||||
return self.doException(merror.IllegalValue)
|
||||
if self.byte_count != self.count * 2:
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, self.count):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
|
||||
result = context.setValues(self.function_code, self.address, self.values)
|
||||
if isinstance(result, ExceptionResponse):
|
||||
return result
|
||||
return WriteMultipleRegistersResponse(self.address, self.count)
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""Get response pdu size.
|
||||
|
||||
Func_code (1 byte) + Starting Address (2 byte) + Quantity of Registers (2 Bytes)
|
||||
:return:
|
||||
"""
|
||||
return 1 + 2 + 2
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of the instance.
|
||||
|
||||
:returns: A string representation of the instance
|
||||
"""
|
||||
params = (self.address, self.count)
|
||||
return (
|
||||
"WriteMultipleRegisterRequest %d => %d" # pylint: disable=consider-using-f-string
|
||||
% params
|
||||
)
|
||||
|
||||
|
||||
class WriteMultipleRegistersResponse(ModbusResponse):
|
||||
"""The normal response returns the function code.
|
||||
|
||||
Starting address, and quantity of registers written.
|
||||
"""
|
||||
|
||||
function_code = 16
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address=None, count=None, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param address: The address to start writing to
|
||||
:param count: The number of registers to write to
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self.address = address
|
||||
self.count = count
|
||||
|
||||
def encode(self):
|
||||
"""Encode a write single register packet packet request.
|
||||
|
||||
:returns: The encoded packet
|
||||
"""
|
||||
return struct.pack(">HH", self.address, self.count)
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode a write single register packet packet request.
|
||||
|
||||
:param data: The request to decode
|
||||
"""
|
||||
self.address, self.count = struct.unpack(">HH", data)
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of the instance.
|
||||
|
||||
:returns: A string representation of the instance
|
||||
"""
|
||||
params = (self.address, self.count)
|
||||
return (
|
||||
"WriteMultipleRegisterResponse (%d,%d)" # pylint: disable=consider-using-f-string
|
||||
% params
|
||||
)
|
||||
|
||||
|
||||
class MaskWriteRegisterRequest(ModbusRequest):
|
||||
"""This function code is used to modify the contents.
|
||||
|
||||
Of a specified holding register using a combination of an AND mask,
|
||||
an OR mask, and the register's current contents.
|
||||
The function can be used to set or clear individual bits in the register.
|
||||
"""
|
||||
|
||||
function_code = 0x16
|
||||
function_code_name = "mask_write_register"
|
||||
_rtu_frame_size = 10
|
||||
|
||||
def __init__(self, address=0x0000, and_mask=0xFFFF, or_mask=0x0000, **kwargs):
|
||||
"""Initialize a new instance.
|
||||
|
||||
:param address: The mask pointer address (0x0000 to 0xffff)
|
||||
:param and_mask: The and bitmask to apply to the register address
|
||||
:param or_mask: The or bitmask to apply to the register address
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self.address = address
|
||||
self.and_mask = and_mask
|
||||
self.or_mask = or_mask
|
||||
|
||||
def encode(self):
|
||||
"""Encode the request packet.
|
||||
|
||||
:returns: The byte encoded packet
|
||||
"""
|
||||
return struct.pack(">HHH", self.address, self.and_mask, self.or_mask)
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode the incoming request.
|
||||
|
||||
:param data: The data to decode into the address
|
||||
"""
|
||||
self.address, self.and_mask, self.or_mask = struct.unpack(">HHH", data)
|
||||
|
||||
def execute(self, context):
|
||||
"""Run a mask write register request against the store.
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: The populated response
|
||||
"""
|
||||
if not 0x0000 <= self.and_mask <= 0xFFFF:
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not 0x0000 <= self.or_mask <= 0xFFFF:
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, 1):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
values = context.getValues(self.function_code, self.address, 1)[0]
|
||||
if isinstance(values, ExceptionResponse):
|
||||
return values
|
||||
values = (values & self.and_mask) | (self.or_mask & ~self.and_mask)
|
||||
result = context.setValues(self.function_code, self.address, [values])
|
||||
if isinstance(result, ExceptionResponse):
|
||||
return result
|
||||
return MaskWriteRegisterResponse(self.address, self.and_mask, self.or_mask)
|
||||
|
||||
|
||||
class MaskWriteRegisterResponse(ModbusResponse):
|
||||
"""The normal response is an echo of the request.
|
||||
|
||||
The response is returned after the register has been written.
|
||||
"""
|
||||
|
||||
function_code = 0x16
|
||||
_rtu_frame_size = 10
|
||||
|
||||
def __init__(self, address=0x0000, and_mask=0xFFFF, or_mask=0x0000, **kwargs):
|
||||
"""Initialize new instance.
|
||||
|
||||
:param address: The mask pointer address (0x0000 to 0xffff)
|
||||
:param and_mask: The and bitmask applied to the register address
|
||||
:param or_mask: The or bitmask applied to the register address
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self.address = address
|
||||
self.and_mask = and_mask
|
||||
self.or_mask = or_mask
|
||||
|
||||
def encode(self):
|
||||
"""Encode the response.
|
||||
|
||||
:returns: The byte encoded message
|
||||
"""
|
||||
return struct.pack(">HHH", self.address, self.and_mask, self.or_mask)
|
||||
|
||||
def decode(self, data):
|
||||
"""Decode a the response.
|
||||
|
||||
:param data: The packet data to decode
|
||||
"""
|
||||
self.address, self.and_mask, self.or_mask = struct.unpack(">HHH", data)
|
||||
1
env/lib/python3.11/site-packages/pymodbus/repl/__init__.py
vendored
Normal file
1
env/lib/python3.11/site-packages/pymodbus/repl/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
"""Pymodbus REPL Module."""
|
||||
BIN
env/lib/python3.11/site-packages/pymodbus/repl/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/repl/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
Binary file not shown.
1
env/lib/python3.11/site-packages/pymodbus/repl/client/__init__.py
vendored
Normal file
1
env/lib/python3.11/site-packages/pymodbus/repl/client/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
"""Repl client."""
|
||||
BIN
env/lib/python3.11/site-packages/pymodbus/repl/client/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/repl/client/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/repl/client/__pycache__/completer.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/repl/client/__pycache__/completer.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/repl/client/__pycache__/helper.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/repl/client/__pycache__/helper.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/repl/client/__pycache__/main.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/repl/client/__pycache__/main.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/repl/client/__pycache__/mclient.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/repl/client/__pycache__/mclient.cpython-311.pyc
vendored
Normal file
Binary file not shown.
143
env/lib/python3.11/site-packages/pymodbus/repl/client/completer.py
vendored
Normal file
143
env/lib/python3.11/site-packages/pymodbus/repl/client/completer.py
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
"""Command Completion for pymodbus REPL."""
|
||||
from prompt_toolkit.application.current import get_app
|
||||
|
||||
# pylint: disable=missing-type-doc
|
||||
from prompt_toolkit.completion import Completer, Completion
|
||||
from prompt_toolkit.filters import Condition
|
||||
from prompt_toolkit.styles import Style
|
||||
|
||||
from pymodbus.repl.client.helper import get_commands
|
||||
|
||||
|
||||
@Condition
|
||||
def has_selected_completion():
|
||||
"""Check for selected completion."""
|
||||
complete_state = get_app().current_buffer.complete_state
|
||||
return complete_state is not None and complete_state.current_completion is not None
|
||||
|
||||
|
||||
style = Style.from_dict(
|
||||
{
|
||||
"completion-menu.completion": "bg:#008888 #ffffff",
|
||||
"completion-menu.completion.current": "bg:#00aaaa #000000",
|
||||
"scrollbar.background": "bg:#88aaaa",
|
||||
"scrollbar.button": "bg:#222222",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class CmdCompleter(Completer):
|
||||
"""Completer for Pymodbus REPL."""
|
||||
|
||||
def __init__(self, client=None, commands=None, ignore_case=True):
|
||||
"""Initialize.
|
||||
|
||||
:param client: Modbus Client
|
||||
:param commands: Commands to be added for Completion (list)
|
||||
:param ignore_case: Ignore Case while looking up for commands
|
||||
"""
|
||||
self._commands = commands or get_commands(client)
|
||||
self._commands["help"] = ""
|
||||
self._command_names = self._commands.keys()
|
||||
self.ignore_case = ignore_case
|
||||
|
||||
@property
|
||||
def commands(self):
|
||||
"""Return commands."""
|
||||
return self._commands
|
||||
|
||||
@property
|
||||
def command_names(self):
|
||||
"""Return command names."""
|
||||
return self._commands.keys()
|
||||
|
||||
def completing_command(self, words, word_before_cursor):
|
||||
"""Determine if we are dealing with supported command.
|
||||
|
||||
:param words: Input text broken in to word tokens.
|
||||
:param word_before_cursor: The current word before the cursor, \
|
||||
which might be one or more blank spaces.
|
||||
:return:
|
||||
"""
|
||||
return len(words) == 1 and len(word_before_cursor)
|
||||
|
||||
def completing_arg(self, words, word_before_cursor):
|
||||
"""Determine if we are currently completing an argument.
|
||||
|
||||
:param words: The input text broken into word tokens.
|
||||
:param word_before_cursor: The current word before the cursor, \
|
||||
which might be one or more blank spaces.
|
||||
:return: Specifies whether we are currently completing an arg.
|
||||
"""
|
||||
return len(words) > 1 and len(word_before_cursor)
|
||||
|
||||
def arg_completions(self, words, _word_before_cursor):
|
||||
"""Generate arguments completions based on the input."""
|
||||
cmd = words[0].strip()
|
||||
cmd = self._commands.get(cmd, None)
|
||||
return cmd if cmd else None
|
||||
|
||||
def _get_completions(self, word, word_before_cursor):
|
||||
"""Get completions."""
|
||||
if self.ignore_case:
|
||||
word_before_cursor = word_before_cursor.lower()
|
||||
return self.word_matches(word, word_before_cursor)
|
||||
|
||||
def word_matches(self, word, word_before_cursor):
|
||||
"""Match the word and word before cursor.
|
||||
|
||||
:param word: The input text broken into word tokens.
|
||||
:param word_before_cursor: The current word before the cursor, \
|
||||
which might be one or more blank spaces.
|
||||
:return: True if matched.
|
||||
|
||||
"""
|
||||
if self.ignore_case:
|
||||
word = word.lower()
|
||||
return word.startswith(word_before_cursor)
|
||||
|
||||
def get_completions(self, document, complete_event):
|
||||
"""Get completions for the current scope.
|
||||
|
||||
:param document: An instance of `prompt_toolkit.Document`.
|
||||
:param complete_event: (Unused).
|
||||
:return: Yields an instance of `prompt_toolkit.completion.Completion`.
|
||||
"""
|
||||
word_before_cursor = document.get_word_before_cursor(WORD=True)
|
||||
text = document.text_before_cursor.lstrip()
|
||||
words = document.text.strip().split()
|
||||
meta = None
|
||||
commands = []
|
||||
if not words:
|
||||
# yield commands
|
||||
pass
|
||||
if self.completing_command(words, word_before_cursor):
|
||||
commands = self._command_names
|
||||
c_meta = {
|
||||
k: v.help_text if not isinstance(v, str) else v
|
||||
for k, v in self._commands.items()
|
||||
}
|
||||
meta = lambda x: ( # pylint: disable=unnecessary-lambda-assignment
|
||||
x,
|
||||
c_meta.get(x, ""),
|
||||
)
|
||||
else:
|
||||
if not list(
|
||||
filter(lambda cmd: any(x == cmd for x in words), self._command_names)
|
||||
):
|
||||
# yield commands
|
||||
pass
|
||||
|
||||
if " " in text:
|
||||
command = self.arg_completions(words, word_before_cursor)
|
||||
commands = list(command.get_completion())
|
||||
commands = list(
|
||||
filter(lambda cmd: not (any(cmd in x for x in words)), commands)
|
||||
)
|
||||
meta = command.get_meta
|
||||
for command in commands:
|
||||
if self._get_completions(command, word_before_cursor):
|
||||
_, display_meta = meta(command) if meta else ("", "")
|
||||
yield Completion(
|
||||
command, -len(word_before_cursor), display_meta=display_meta
|
||||
)
|
||||
312
env/lib/python3.11/site-packages/pymodbus/repl/client/helper.py
vendored
Normal file
312
env/lib/python3.11/site-packages/pymodbus/repl/client/helper.py
vendored
Normal file
@@ -0,0 +1,312 @@
|
||||
"""Helper Module for REPL actions."""
|
||||
import inspect
|
||||
|
||||
# pylint: disable=missing-type-doc
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
import pygments
|
||||
from prompt_toolkit import print_formatted_text
|
||||
from prompt_toolkit.formatted_text import HTML, PygmentsTokens
|
||||
from pygments.lexers.data import JsonLexer
|
||||
|
||||
from pymodbus.payload import BinaryPayloadDecoder, Endian
|
||||
|
||||
|
||||
predicate = inspect.isfunction
|
||||
argspec = inspect.signature
|
||||
|
||||
|
||||
FORMATTERS = {
|
||||
"int8": "decode_8bit_int",
|
||||
"int16": "decode_16bit_int",
|
||||
"int32": "decode_32bit_int",
|
||||
"int64": "decode_64bit_int",
|
||||
"uint8": "decode_8bit_uint",
|
||||
"uint16": "decode_16bit_uint",
|
||||
"uint32": "decode_32bit_uint",
|
||||
"uint64": "decode_64bit_int",
|
||||
"float16": "decode_16bit_float",
|
||||
"float32": "decode_32bit_float",
|
||||
"float64": "decode_64bit_float",
|
||||
}
|
||||
|
||||
|
||||
DEFAULT_KWARGS = {"slave": "Slave address"}
|
||||
|
||||
OTHER_COMMANDS = {
|
||||
"result.raw": "Show RAW Result",
|
||||
"result.decode": "Decode register response to known formats",
|
||||
}
|
||||
EXCLUDE = ["execute", "recv", "send", "trace", "set_debug"]
|
||||
CLIENT_METHODS = [
|
||||
"connect",
|
||||
"close",
|
||||
"idle_time",
|
||||
"is_socket_open",
|
||||
"get_port",
|
||||
"set_port",
|
||||
"get_stopbits",
|
||||
"set_stopbits",
|
||||
"get_bytesize",
|
||||
"set_bytesize",
|
||||
"get_parity",
|
||||
"set_parity",
|
||||
"get_baudrate",
|
||||
"set_baudrate",
|
||||
"get_timeout",
|
||||
"set_timeout",
|
||||
"get_serial_settings",
|
||||
]
|
||||
CLIENT_ATTRIBUTES: List[str] = []
|
||||
|
||||
|
||||
class Command:
|
||||
"""Class representing Commands to be consumed by Completer."""
|
||||
|
||||
def __init__(self, name, signature, doc, slave=False):
|
||||
"""Initialize.
|
||||
|
||||
:param name: Name of the command
|
||||
:param signature: inspect object
|
||||
:param doc: Doc string for the command
|
||||
:param slave: Use slave as additional argument in the command .
|
||||
"""
|
||||
self.name = name
|
||||
self.doc = doc.split("\n") if doc else " ".join(name.split("_"))
|
||||
self.help_text = self._create_help()
|
||||
self.param_help = self._create_arg_help()
|
||||
if signature:
|
||||
self._params = signature.parameters
|
||||
self.args = self.create_completion()
|
||||
else:
|
||||
self._params = ""
|
||||
|
||||
if self.name.startswith("client.") and slave:
|
||||
self.args.update(**DEFAULT_KWARGS)
|
||||
|
||||
def _create_help(self):
|
||||
"""Create help."""
|
||||
doc = filter(lambda d: d, self.doc)
|
||||
cmd_help = list(
|
||||
filter(
|
||||
lambda x: not x.startswith(":param") and not x.startswith(":return"),
|
||||
doc,
|
||||
)
|
||||
)
|
||||
return " ".join(cmd_help).strip()
|
||||
|
||||
def _create_arg_help(self):
|
||||
"""Create arg help."""
|
||||
param_dict = {}
|
||||
params = list(filter(lambda d: d.strip().startswith(":param"), self.doc))
|
||||
for param in params:
|
||||
param, param_help = param.split(":param")[1].strip().split(":")
|
||||
param_dict[param] = param_help
|
||||
return param_dict
|
||||
|
||||
def create_completion(self):
|
||||
"""Create command completion meta data.
|
||||
|
||||
:return:
|
||||
"""
|
||||
words = {}
|
||||
|
||||
def _create(entry, default):
|
||||
if entry not in ["self", "kwargs"]:
|
||||
if isinstance(default, (int, str)):
|
||||
entry += f"={default}"
|
||||
return entry
|
||||
return None
|
||||
|
||||
for arg in self._params.values():
|
||||
if entry := _create(arg.name, arg.default):
|
||||
entry, meta = self.get_meta(entry)
|
||||
words[entry] = meta
|
||||
|
||||
return words
|
||||
|
||||
def get_completion(self):
|
||||
"""Get a list of completions.
|
||||
|
||||
:return:
|
||||
"""
|
||||
return self.args.keys()
|
||||
|
||||
def get_meta(self, cmd):
|
||||
"""Get Meta info of a given command.
|
||||
|
||||
:param cmd: Name of command.
|
||||
:return: Dict containing meta info.
|
||||
"""
|
||||
cmd = cmd.strip()
|
||||
cmd = cmd.split("=")[0].strip()
|
||||
return cmd, self.param_help.get(cmd, "")
|
||||
|
||||
def __str__(self):
|
||||
"""Return string representation."""
|
||||
if self.doc:
|
||||
return f"Command {self.name:>50}{self.doc:<20}"
|
||||
return f"Command {self.name}"
|
||||
|
||||
|
||||
def _get_requests(members):
|
||||
"""Get requests."""
|
||||
commands = list(
|
||||
filter(
|
||||
lambda x: (
|
||||
x[0] not in EXCLUDE and x[0] not in CLIENT_METHODS and callable(x[1])
|
||||
),
|
||||
members,
|
||||
)
|
||||
)
|
||||
commands = {
|
||||
f"client.{c[0]}": Command(
|
||||
f"client.{c[0]}", argspec(c[1]), inspect.getdoc(c[1]), slave=False
|
||||
)
|
||||
for c in commands
|
||||
if not c[0].startswith("_")
|
||||
}
|
||||
return commands
|
||||
|
||||
|
||||
def _get_client_methods(members):
|
||||
"""Get client methods."""
|
||||
commands = list(
|
||||
filter(lambda x: (x[0] not in EXCLUDE and x[0] in CLIENT_METHODS), members)
|
||||
)
|
||||
commands = {
|
||||
f"client.{c[0]}": Command(
|
||||
f"client.{c[0]}", argspec(c[1]), inspect.getdoc(c[1]), slave=False
|
||||
)
|
||||
for c in commands
|
||||
if not c[0].startswith("_")
|
||||
}
|
||||
return commands
|
||||
|
||||
|
||||
def _get_client_properties(members):
|
||||
"""Get client properties."""
|
||||
global CLIENT_ATTRIBUTES # pylint: disable=global-variable-not-assigned
|
||||
commands = list(filter(lambda x: not callable(x[1]), members))
|
||||
commands = {
|
||||
f"client.{c[0]}": Command(f"client.{c[0]}", None, "Read Only!", slave=False)
|
||||
for c in commands
|
||||
if (not c[0].startswith("_") and isinstance(c[1], (str, int, float)))
|
||||
}
|
||||
CLIENT_ATTRIBUTES.extend(list(commands.keys()))
|
||||
return commands
|
||||
|
||||
|
||||
def get_commands(client):
|
||||
"""Retrieve all required methods and attributes.
|
||||
|
||||
Of a client object and convert it to commands.
|
||||
|
||||
:param client: Modbus Client object.
|
||||
:return:
|
||||
"""
|
||||
commands = {}
|
||||
members = inspect.getmembers(client)
|
||||
requests = _get_requests(members)
|
||||
client_methods = _get_client_methods(members)
|
||||
client_attr = _get_client_properties(members)
|
||||
|
||||
result_commands = inspect.getmembers(Result, predicate=predicate)
|
||||
result_commands = {
|
||||
f"result.{c[0]}": Command(f"result.{c[0]}", argspec(c[1]), inspect.getdoc(c[1]))
|
||||
for c in result_commands
|
||||
if (not c[0].startswith("_") and c[0] != "print_result")
|
||||
}
|
||||
commands.update(requests)
|
||||
commands.update(client_methods)
|
||||
commands.update(client_attr)
|
||||
commands.update(result_commands)
|
||||
return commands
|
||||
|
||||
|
||||
class Result:
|
||||
"""Represent result command."""
|
||||
|
||||
function_code: int = None
|
||||
data: Union[Dict[int, Any], Any] = None
|
||||
|
||||
def __init__(self, result):
|
||||
"""Initialize.
|
||||
|
||||
:param result: Response of a modbus command.
|
||||
"""
|
||||
if isinstance(result, dict): # Modbus response
|
||||
self.function_code = result.pop("function_code", None)
|
||||
self.data = dict(result)
|
||||
else:
|
||||
self.data = result
|
||||
|
||||
def decode(self, formatters, byte_order="big", word_order="big"):
|
||||
"""Decode the register response to known formatters.
|
||||
|
||||
:param formatters: int8/16/32/64, uint8/16/32/64, float32/64
|
||||
:param byte_order: little/big
|
||||
:param word_order: little/big
|
||||
"""
|
||||
# Read Holding Registers (3)
|
||||
# Read Input Registers (4)
|
||||
# Read Write Registers (23)
|
||||
if not isinstance(formatters, (list, tuple)):
|
||||
formatters = [formatters]
|
||||
|
||||
if self.function_code not in [3, 4, 23]:
|
||||
print_formatted_text(HTML("<red>Decoder works only for registers!!</red>"))
|
||||
return
|
||||
byte_order = (
|
||||
Endian.LITTLE if byte_order.strip().lower() == "little" else Endian.BIG
|
||||
)
|
||||
word_order = (
|
||||
Endian.LITTLE if word_order.strip().lower() == "little" else Endian.BIG
|
||||
)
|
||||
decoder = BinaryPayloadDecoder.fromRegisters(
|
||||
self.data.get("registers"), byteorder=byte_order, wordorder=word_order
|
||||
)
|
||||
for formatter in formatters:
|
||||
if not (formatter := FORMATTERS.get(formatter)):
|
||||
print_formatted_text(
|
||||
HTML(f"<red>Invalid Formatter - {formatter}!!</red>")
|
||||
)
|
||||
return
|
||||
decoded = getattr(decoder, formatter)()
|
||||
self.print_result(decoded)
|
||||
|
||||
def raw(self):
|
||||
"""Return raw result dict."""
|
||||
self.print_result()
|
||||
|
||||
def _process_dict(self, use_dict):
|
||||
"""Process dict."""
|
||||
new_dict = OrderedDict()
|
||||
for k, v_item in use_dict.items():
|
||||
if isinstance(v_item, bytes):
|
||||
v_item = v_item.decode("utf-8")
|
||||
elif isinstance(v_item, dict):
|
||||
v_item = self._process_dict(v_item)
|
||||
elif isinstance(v_item, (list, tuple)):
|
||||
v_item = [
|
||||
v1.decode("utf-8") if isinstance(v1, bytes) else v1 for v1 in v_item
|
||||
]
|
||||
new_dict[k] = v_item
|
||||
return new_dict
|
||||
|
||||
def print_result(self, data=None):
|
||||
"""Print result object pretty.
|
||||
|
||||
:param data: Data to be printed.
|
||||
"""
|
||||
data = data or self.data
|
||||
if isinstance(data, dict):
|
||||
data = self._process_dict(data)
|
||||
elif isinstance(data, (list, tuple)):
|
||||
data = [v.decode("utf-8") if isinstance(v, bytes) else v for v in data]
|
||||
elif isinstance(data, bytes):
|
||||
data = data.decode("utf-8")
|
||||
tokens = list(pygments.lex(json.dumps(data, indent=4), lexer=JsonLexer()))
|
||||
print_formatted_text(PygmentsTokens(tokens))
|
||||
437
env/lib/python3.11/site-packages/pymodbus/repl/client/main.py
vendored
Normal file
437
env/lib/python3.11/site-packages/pymodbus/repl/client/main.py
vendored
Normal file
@@ -0,0 +1,437 @@
|
||||
"""Pymodbus REPL Entry point."""
|
||||
import logging
|
||||
import pathlib
|
||||
|
||||
import click
|
||||
from prompt_toolkit import PromptSession, print_formatted_text
|
||||
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||||
from prompt_toolkit.formatted_text import HTML
|
||||
from prompt_toolkit.history import FileHistory
|
||||
from prompt_toolkit.key_binding import KeyBindings
|
||||
from prompt_toolkit.lexers import PygmentsLexer
|
||||
from prompt_toolkit.styles import Style
|
||||
from pygments.lexers.python import PythonLexer
|
||||
|
||||
from pymodbus import __version__ as pymodbus_version
|
||||
from pymodbus.exceptions import ParameterException
|
||||
from pymodbus.repl.client.completer import (
|
||||
CmdCompleter,
|
||||
has_selected_completion,
|
||||
)
|
||||
from pymodbus.repl.client.helper import CLIENT_ATTRIBUTES, Result
|
||||
from pymodbus.repl.client.mclient import ModbusSerialClient, ModbusTcpClient
|
||||
from pymodbus.transaction import (
|
||||
ModbusAsciiFramer,
|
||||
ModbusBinaryFramer,
|
||||
ModbusRtuFramer,
|
||||
ModbusSocketFramer,
|
||||
)
|
||||
|
||||
|
||||
_logger = logging.getLogger()
|
||||
|
||||
TITLE = rf"""
|
||||
----------------------------------------------------------------------------
|
||||
__________ _____ .___ __________ .__
|
||||
\______ \___.__. / \ ____ __| _/ \______ \ ____ ______ | |
|
||||
| ___< | |/ \ / \ / _ \ / __ | | _// __ \\\____ \| |
|
||||
| | \___ / Y ( <_> ) /_/ | | | \ ___/| |_> > |__
|
||||
|____| / ____\____|__ /\____/\____ | /\ |____|_ /\___ > __/|____/
|
||||
\/ \/ \/ \/ \/ \/|__|
|
||||
v1.3.0 - {pymodbus_version}
|
||||
----------------------------------------------------------------------------
|
||||
"""
|
||||
|
||||
|
||||
style = Style.from_dict(
|
||||
{
|
||||
"completion-menu.completion": "bg:#008888 #ffffff",
|
||||
"completion-menu.completion.current": "bg:#00aaaa #000000",
|
||||
"scrollbar.background": "bg:#88aaaa",
|
||||
"scrollbar.button": "bg:#222222",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def bottom_toolbar():
|
||||
"""Do console toolbar.
|
||||
|
||||
:return:
|
||||
"""
|
||||
return HTML(
|
||||
'Press <b><style bg="ansired">CTRL+D or exit </style></b>'
|
||||
' to exit! Type "help" for list of available commands'
|
||||
)
|
||||
|
||||
|
||||
class CaseInsenstiveChoice(click.Choice):
|
||||
"""Do case Insensitive choice for click commands and options."""
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
"""Convert args to uppercase for evaluation."""
|
||||
if value is None:
|
||||
return None
|
||||
return super().convert(value.strip().upper(), param, ctx)
|
||||
|
||||
|
||||
class NumericChoice(click.Choice):
|
||||
"""Do numeric choice for click arguments and options."""
|
||||
|
||||
def __init__(self, choices, typ):
|
||||
"""Initialize."""
|
||||
self.typ = typ
|
||||
super().__init__(choices)
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
"""Convert."""
|
||||
# Exact match
|
||||
if value in self.choices:
|
||||
return self.typ(value)
|
||||
|
||||
if ctx is not None and ctx.token_normalize_func is not None:
|
||||
value = ctx.token_normalize_func(value)
|
||||
for choice in self.casted_choices: # pylint: disable=no-member
|
||||
if ctx.token_normalize_func(choice) == value:
|
||||
return choice
|
||||
|
||||
self.fail(
|
||||
f"invalid choice: {value}. (choose from {', '.join(self.choices)})",
|
||||
param,
|
||||
ctx,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _process_args(args: list, string: bool = True):
|
||||
"""Parse arguments provided on command line.
|
||||
|
||||
:param args: Array of argument values
|
||||
:param string: True if arguments values are strings, false if argument values are integers
|
||||
|
||||
:return Tuple, where the first member is hash of parsed values, and second is boolean flag
|
||||
indicating if parsing succeeded.
|
||||
"""
|
||||
kwargs = {}
|
||||
execute = True
|
||||
skip_index = None
|
||||
|
||||
def _parse_val(arg_name, val):
|
||||
if not string:
|
||||
if "," in val:
|
||||
val = val.split(",")
|
||||
val = [int(v, 0) for v in val]
|
||||
else:
|
||||
val = int(val, 0)
|
||||
kwargs[arg_name] = val
|
||||
|
||||
for i, arg in enumerate(args):
|
||||
if i == skip_index:
|
||||
continue
|
||||
arg = arg.strip()
|
||||
if "=" in arg:
|
||||
arg_name, val = arg.split("=")
|
||||
_parse_val(arg_name, val)
|
||||
else:
|
||||
arg_name, val = arg, args[i + 1]
|
||||
try:
|
||||
_parse_val(arg_name, val)
|
||||
skip_index = i + 1
|
||||
except TypeError:
|
||||
click.secho("Error parsing arguments!", fg="yellow")
|
||||
execute = False
|
||||
break
|
||||
except ValueError:
|
||||
click.secho("Error parsing argument", fg="yellow")
|
||||
execute = False
|
||||
break
|
||||
return kwargs, execute
|
||||
|
||||
|
||||
class CLI: # pylint: disable=too-few-public-methods
|
||||
"""Client definition."""
|
||||
|
||||
def __init__(self, client):
|
||||
"""Set up client and keybindings."""
|
||||
|
||||
use_keys = KeyBindings()
|
||||
history_file = pathlib.Path.home().joinpath(".pymodhis")
|
||||
self.client = client
|
||||
|
||||
@use_keys.add("c-space")
|
||||
def _(event):
|
||||
"""Initialize autocompletion, or select the next completion."""
|
||||
buff = event.app.current_buffer
|
||||
if buff.complete_state:
|
||||
buff.complete_next()
|
||||
else:
|
||||
buff.start_completion(select_first=False)
|
||||
|
||||
@use_keys.add("enter", filter=has_selected_completion)
|
||||
def _(event):
|
||||
"""Make the enter key work as the tab key only when showing the menu."""
|
||||
event.current_buffer.complete_state = None
|
||||
buffer = event.cli.current_buffer
|
||||
buffer.complete_state = None
|
||||
|
||||
self.session = PromptSession(
|
||||
lexer=PygmentsLexer(PythonLexer),
|
||||
completer=CmdCompleter(client),
|
||||
style=style,
|
||||
complete_while_typing=True,
|
||||
bottom_toolbar=bottom_toolbar,
|
||||
key_bindings=use_keys,
|
||||
history=FileHistory(history_file),
|
||||
auto_suggest=AutoSuggestFromHistory(),
|
||||
)
|
||||
click.secho(TITLE, fg="green")
|
||||
|
||||
def _print_command_help(self, commands):
|
||||
"""Print a list of commands with help text."""
|
||||
for cmd, obj in sorted(commands.items()):
|
||||
if cmd != "help":
|
||||
print_formatted_text(
|
||||
HTML(
|
||||
f"<skyblue>{cmd:45s}</skyblue>"
|
||||
f"<seagreen>{obj.help_text:100s}"
|
||||
"</seagreen>"
|
||||
)
|
||||
)
|
||||
|
||||
def _process_client(self, text, client) -> Result:
|
||||
"""Process client commands."""
|
||||
text = text.strip().split()
|
||||
cmd = text[0].split(".")[1]
|
||||
args = text[1:]
|
||||
kwargs, execute = _process_args(args, string=False)
|
||||
if execute:
|
||||
if text[0] in CLIENT_ATTRIBUTES:
|
||||
result = Result(getattr(client, cmd))
|
||||
else:
|
||||
result = Result(getattr(client, cmd)(**kwargs))
|
||||
result.print_result()
|
||||
return result
|
||||
|
||||
def _process_result(self, text, result):
|
||||
"""Process result commands."""
|
||||
words = text.split()
|
||||
if words[0] == "result.raw":
|
||||
result.raw()
|
||||
if words[0] == "result.decode":
|
||||
args = words[1:]
|
||||
kwargs, execute = _process_args(args)
|
||||
if execute:
|
||||
result.decode(**kwargs)
|
||||
|
||||
def run(self):
|
||||
"""Run the REPL."""
|
||||
result = None
|
||||
while True:
|
||||
try:
|
||||
text = self.session.prompt("> ", complete_while_typing=True)
|
||||
if text.strip().lower() == "help":
|
||||
print_formatted_text(HTML("<u>Available commands:</u>"))
|
||||
self._print_command_help(self.session.completer.commands)
|
||||
elif text.strip().lower() == "exit":
|
||||
raise EOFError()
|
||||
elif text.strip().lower().startswith("client."):
|
||||
result = self._process_client(text, self.client)
|
||||
elif text.strip().lower().startswith("result.") and result:
|
||||
self._process_result(text, result)
|
||||
except KeyboardInterrupt:
|
||||
continue # Control-C pressed. Try again.
|
||||
except EOFError:
|
||||
break # Control-D pressed.
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
click.secho(str(exc), fg="red")
|
||||
|
||||
click.secho("GoodBye!", fg="blue")
|
||||
|
||||
|
||||
@click.group("pymodbus-repl")
|
||||
@click.version_option(str(pymodbus_version), message=TITLE)
|
||||
@click.option("--verbose", is_flag=True, default=False, help="Verbose logs")
|
||||
@click.option(
|
||||
"--broadcast-support",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Support broadcast messages",
|
||||
)
|
||||
@click.option(
|
||||
"--retry-on-empty", is_flag=True, default=False, help="Retry on empty response"
|
||||
)
|
||||
@click.option(
|
||||
"--retry-on-error", is_flag=True, default=False, help="Retry on error response"
|
||||
)
|
||||
@click.option("--retries", default=3, help="Retry count")
|
||||
@click.pass_context
|
||||
def main(
|
||||
ctx,
|
||||
verbose,
|
||||
broadcast_support,
|
||||
retry_on_empty,
|
||||
retry_on_error,
|
||||
retries,
|
||||
):
|
||||
"""Run Main."""
|
||||
if verbose:
|
||||
use_format = (
|
||||
"%(asctime)-15s %(threadName)-15s "
|
||||
"%(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s"
|
||||
)
|
||||
logging.basicConfig(format=use_format)
|
||||
_logger.setLevel(logging.DEBUG)
|
||||
ctx.obj = {
|
||||
"broadcast_enable": broadcast_support,
|
||||
"retry_on_empty": retry_on_empty,
|
||||
"retry_on_invalid": retry_on_error,
|
||||
"retries": retries,
|
||||
}
|
||||
|
||||
|
||||
@main.command("tcp")
|
||||
@click.pass_context
|
||||
@click.option("--host", default="localhost", help="Modbus TCP IP ")
|
||||
@click.option(
|
||||
"--port",
|
||||
default=502,
|
||||
type=int,
|
||||
help="Modbus TCP port",
|
||||
)
|
||||
@click.option(
|
||||
"--framer",
|
||||
default="tcp",
|
||||
type=str,
|
||||
help="Override the default packet framer tcp|rtu",
|
||||
)
|
||||
def tcp(ctx, host, port, framer):
|
||||
"""Define TCP."""
|
||||
kwargs = {"host": host, "port": port}
|
||||
kwargs.update(**ctx.obj)
|
||||
if framer == "rtu":
|
||||
kwargs["framer"] = ModbusRtuFramer
|
||||
client = ModbusTcpClient(**kwargs)
|
||||
cli = CLI(client)
|
||||
cli.run()
|
||||
|
||||
|
||||
@main.command("serial")
|
||||
@click.pass_context
|
||||
@click.option(
|
||||
"--method",
|
||||
default="rtu",
|
||||
type=str,
|
||||
help="Modbus Serial Mode (rtu/ascii)",
|
||||
)
|
||||
@click.option(
|
||||
"--port",
|
||||
default=None,
|
||||
type=str,
|
||||
help="Modbus RTU port",
|
||||
)
|
||||
@click.option(
|
||||
"--baudrate",
|
||||
help="Modbus RTU serial baudrate to use.",
|
||||
default=9600,
|
||||
type=int,
|
||||
)
|
||||
@click.option(
|
||||
"--bytesize",
|
||||
help="Modbus RTU serial Number of data bits. "
|
||||
"Possible values: FIVEBITS, SIXBITS, SEVENBITS, "
|
||||
"EIGHTBITS.",
|
||||
type=NumericChoice(["5", "6", "7", "8"], int),
|
||||
default="8",
|
||||
)
|
||||
@click.option(
|
||||
"--parity",
|
||||
help="Modbus RTU serial parity. "
|
||||
" Enable parity checking. Possible values: "
|
||||
"PARITY_NONE, PARITY_EVEN, PARITY_ODD PARITY_MARK, "
|
||||
'PARITY_SPACE. Default to "N"',
|
||||
default="N",
|
||||
type=CaseInsenstiveChoice(["N", "E", "O", "M", "S"]),
|
||||
)
|
||||
@click.option(
|
||||
"--stopbits",
|
||||
help="Modbus RTU serial stop bits. "
|
||||
"Number of stop bits. Possible values: STOPBITS_ONE, "
|
||||
'STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO. Default to "1"',
|
||||
default="1",
|
||||
type=NumericChoice(["1", "1.5", "2"], float),
|
||||
)
|
||||
@click.option(
|
||||
"--xonxoff",
|
||||
help="Modbus RTU serial xonxoff. Enable software flow control.",
|
||||
default=0,
|
||||
type=int,
|
||||
)
|
||||
@click.option(
|
||||
"--rtscts",
|
||||
help="Modbus RTU serial rtscts. Enable hardware (RTS/CTS) flow " "control.",
|
||||
default=0,
|
||||
type=int,
|
||||
)
|
||||
@click.option(
|
||||
"--dsrdtr",
|
||||
help="Modbus RTU serial dsrdtr. Enable hardware (DSR/DTR) flow " "control.",
|
||||
default=0,
|
||||
type=int,
|
||||
)
|
||||
@click.option(
|
||||
"--timeout",
|
||||
help="Modbus RTU serial read timeout.",
|
||||
default=0.25,
|
||||
type=float,
|
||||
)
|
||||
@click.option(
|
||||
"--write-timeout",
|
||||
help="Modbus RTU serial write timeout.",
|
||||
default=2,
|
||||
type=float,
|
||||
)
|
||||
def serial( # pylint: disable=too-many-arguments
|
||||
ctx,
|
||||
method,
|
||||
port,
|
||||
baudrate,
|
||||
bytesize,
|
||||
parity,
|
||||
stopbits,
|
||||
xonxoff,
|
||||
rtscts,
|
||||
dsrdtr,
|
||||
timeout,
|
||||
write_timeout,
|
||||
):
|
||||
"""Define serial communication."""
|
||||
method = method.lower()
|
||||
if method == "ascii":
|
||||
framer = ModbusAsciiFramer
|
||||
elif method == "rtu":
|
||||
framer = ModbusRtuFramer
|
||||
elif method == "binary":
|
||||
framer = ModbusBinaryFramer
|
||||
elif method == "socket":
|
||||
framer = ModbusSocketFramer
|
||||
else:
|
||||
raise ParameterException("Invalid framer method requested")
|
||||
client = ModbusSerialClient(
|
||||
framer=framer,
|
||||
port=port,
|
||||
baudrate=baudrate,
|
||||
bytesize=bytesize,
|
||||
parity=parity,
|
||||
stopbits=stopbits,
|
||||
xonxoff=xonxoff,
|
||||
rtscts=rtscts,
|
||||
dsrdtr=dsrdtr,
|
||||
timeout=timeout,
|
||||
write_timeout=write_timeout,
|
||||
**ctx.obj,
|
||||
)
|
||||
cli = CLI(client)
|
||||
cli.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main() # pylint: disable=no-value-for-parameter
|
||||
683
env/lib/python3.11/site-packages/pymodbus/repl/client/mclient.py
vendored
Normal file
683
env/lib/python3.11/site-packages/pymodbus/repl/client/mclient.py
vendored
Normal file
@@ -0,0 +1,683 @@
|
||||
"""Modbus Clients to be used with REPL."""
|
||||
# pylint: disable=missing-type-doc
|
||||
import functools
|
||||
|
||||
from pymodbus.client import ModbusSerialClient as _ModbusSerialClient
|
||||
from pymodbus.client import ModbusTcpClient as _ModbusTcpClient
|
||||
from pymodbus.diag_message import (
|
||||
ChangeAsciiInputDelimiterRequest,
|
||||
ClearCountersRequest,
|
||||
ClearOverrunCountRequest,
|
||||
ForceListenOnlyModeRequest,
|
||||
GetClearModbusPlusRequest,
|
||||
RestartCommunicationsOptionRequest,
|
||||
ReturnBusCommunicationErrorCountRequest,
|
||||
ReturnBusExceptionErrorCountRequest,
|
||||
ReturnBusMessageCountRequest,
|
||||
ReturnDiagnosticRegisterRequest,
|
||||
ReturnIopOverrunCountRequest,
|
||||
ReturnQueryDataRequest,
|
||||
ReturnSlaveBusCharacterOverrunCountRequest,
|
||||
ReturnSlaveBusyCountRequest,
|
||||
ReturnSlaveMessageCountRequest,
|
||||
ReturnSlaveNAKCountRequest,
|
||||
ReturnSlaveNoResponseCountRequest,
|
||||
)
|
||||
from pymodbus.exceptions import ModbusIOException
|
||||
from pymodbus.mei_message import ReadDeviceInformationRequest
|
||||
from pymodbus.other_message import (
|
||||
GetCommEventCounterRequest,
|
||||
GetCommEventLogRequest,
|
||||
ReadExceptionStatusRequest,
|
||||
ReportSlaveIdRequest,
|
||||
)
|
||||
from pymodbus.pdu import ExceptionResponse, ModbusExceptions
|
||||
|
||||
|
||||
def make_response_dict(resp):
|
||||
"""Make response dict."""
|
||||
resp_dict = {"function_code": resp.function_code, "address": resp.address}
|
||||
if hasattr(resp, "value"):
|
||||
resp_dict["value"] = resp.value
|
||||
elif hasattr(resp, "values"):
|
||||
resp_dict["values"] = resp.values
|
||||
elif hasattr(resp, "count"):
|
||||
resp_dict["count"] = resp.count
|
||||
return resp_dict
|
||||
|
||||
|
||||
def handle_brodcast(func):
|
||||
"""Handle broadcast."""
|
||||
|
||||
@functools.wraps(func)
|
||||
def _wrapper(*args, **kwargs):
|
||||
self = args[0]
|
||||
resp = func(*args, **kwargs)
|
||||
if not kwargs.get("slave") and self.params.broadcast_enable:
|
||||
return {"broadcasted": True}
|
||||
if not resp.isError():
|
||||
return make_response_dict(resp)
|
||||
return ExtendedRequestSupport._process_exception( # pylint: disable=protected-access
|
||||
resp, **kwargs
|
||||
)
|
||||
|
||||
return _wrapper
|
||||
|
||||
|
||||
class ExtendedRequestSupport: # pylint: disable=(too-many-public-methods
|
||||
"""Extended request support."""
|
||||
|
||||
@staticmethod
|
||||
def _process_exception(resp, **kwargs):
|
||||
"""Set internal process exception."""
|
||||
if "slave" not in kwargs:
|
||||
err = {"message": "Broadcast message, ignoring errors!!!"}
|
||||
else:
|
||||
if isinstance(resp, ExceptionResponse): # pylint: disable=else-if-used
|
||||
err = {
|
||||
"original_function_code": f"{resp.original_code} ({hex(resp.original_code)})",
|
||||
"error_function_code": f"{resp.function_code} ({hex(resp.function_code)})",
|
||||
"exception code": resp.exception_code,
|
||||
"message": ModbusExceptions.decode(resp.exception_code),
|
||||
}
|
||||
elif isinstance(resp, ModbusIOException):
|
||||
err = {
|
||||
"original_function_code": f"{resp.fcode} ({hex(resp.fcode)})",
|
||||
"error": resp.message,
|
||||
}
|
||||
else:
|
||||
err = {"error": str(resp)}
|
||||
return err
|
||||
|
||||
def read_coils(self, address, count=1, slave=0, **kwargs):
|
||||
"""Read `count` coils from a given slave starting at `address`.
|
||||
|
||||
:param address: The starting address to read from
|
||||
:param count: The number of coils to read
|
||||
:param slave: Modbus slave slave ID
|
||||
:param kwargs:
|
||||
:returns: List of register values
|
||||
"""
|
||||
resp = super().read_coils( # pylint: disable=no-member
|
||||
address, count, slave, **kwargs
|
||||
)
|
||||
if not resp.isError():
|
||||
return {"function_code": resp.function_code, "bits": resp.bits}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=slave)
|
||||
|
||||
def read_discrete_inputs(self, address, count=1, slave=0, **kwargs):
|
||||
"""Read `count` number of discrete inputs starting at offset `address`.
|
||||
|
||||
:param address: The starting address to read from
|
||||
:param count: The number of coils to read
|
||||
:param slave: Modbus slave slave ID
|
||||
:param kwargs:
|
||||
:return: List of bits
|
||||
"""
|
||||
resp = super().read_discrete_inputs( # pylint: disable=no-member
|
||||
address, count, slave, **kwargs
|
||||
)
|
||||
if not resp.isError():
|
||||
return {"function_code": resp.function_code, "bits": resp.bits}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=slave)
|
||||
|
||||
@handle_brodcast
|
||||
def write_coil(self, address, value, slave=0, **kwargs):
|
||||
"""Write `value` to coil at `address`.
|
||||
|
||||
:param address: coil offset to write to
|
||||
:param value: bit value to write
|
||||
:param slave: Modbus slave slave ID
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
resp = super().write_coil( # pylint: disable=no-member
|
||||
address, value, slave, **kwargs
|
||||
)
|
||||
return resp
|
||||
|
||||
@handle_brodcast
|
||||
def write_coils(self, address, values, slave=0, **kwargs):
|
||||
"""Write `value` to coil at `address`.
|
||||
|
||||
:param address: coil offset to write to
|
||||
:param values: list of bit values to write (comma separated)
|
||||
:param slave: Modbus slave slave ID
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
resp = super().write_coils( # pylint: disable=no-member
|
||||
address, values, slave, **kwargs
|
||||
)
|
||||
return resp
|
||||
|
||||
@handle_brodcast
|
||||
def write_register(self, address, value, slave=0, **kwargs):
|
||||
"""Write `value` to register at `address`.
|
||||
|
||||
:param address: register offset to write to
|
||||
:param value: register value to write
|
||||
:param slave: Modbus slave slave ID
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
resp = super().write_register( # pylint: disable=no-member
|
||||
address, value, slave, **kwargs
|
||||
)
|
||||
return resp
|
||||
|
||||
@handle_brodcast
|
||||
def write_registers(self, address, values, slave=0, **kwargs):
|
||||
"""Write list of `values` to registers starting at `address`.
|
||||
|
||||
:param address: register offset to write to
|
||||
:param values: list of register value to write (comma separated)
|
||||
:param slave: Modbus slave slave ID
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
resp = super().write_registers( # pylint: disable=no-member
|
||||
address, values, slave, **kwargs
|
||||
)
|
||||
return resp
|
||||
|
||||
def read_holding_registers(self, address, count=1, slave=0, **kwargs):
|
||||
"""Read `count` number of holding registers starting at `address`.
|
||||
|
||||
:param address: starting register offset to read from
|
||||
:param count: Number of registers to read
|
||||
:param slave: Modbus slave slave ID
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
resp = super().read_holding_registers( # pylint: disable=no-member
|
||||
address, count, slave, **kwargs
|
||||
)
|
||||
if not resp.isError():
|
||||
return {"function_code": resp.function_code, "registers": resp.registers}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=slave)
|
||||
|
||||
def read_input_registers(self, address, count=1, slave=0, **kwargs):
|
||||
"""Read `count` number of input registers starting at `address`.
|
||||
|
||||
:param address: starting register offset to read from to
|
||||
:param count: Number of registers to read
|
||||
:param slave: Modbus slave slave ID
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
resp = super().read_input_registers( # pylint: disable=no-member
|
||||
address, count, slave, **kwargs
|
||||
)
|
||||
if not resp.isError():
|
||||
return {"function_code": resp.function_code, "registers": resp.registers}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=slave)
|
||||
|
||||
def readwrite_registers(
|
||||
self,
|
||||
read_address=0,
|
||||
read_count=0,
|
||||
write_address=0,
|
||||
values=0,
|
||||
slave=0,
|
||||
**kwargs,
|
||||
):
|
||||
"""Read `read_count` number of holding registers.
|
||||
|
||||
Starting at `read_address`
|
||||
and write `write_registers` starting at `write_address`.
|
||||
|
||||
:param read_address: register offset to read from
|
||||
:param read_count: Number of registers to read
|
||||
:param write_address: register offset to write to
|
||||
:param values: List of register values to write (comma separated)
|
||||
:param slave: Modbus slave slave ID
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
resp = super().readwrite_registers( # pylint: disable=no-member
|
||||
read_address=read_address,
|
||||
read_count=read_count,
|
||||
write_address=write_address,
|
||||
values=values,
|
||||
slave=slave,
|
||||
**kwargs,
|
||||
)
|
||||
if not resp.isError():
|
||||
return {"function_code": resp.function_code, "registers": resp.registers}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=slave)
|
||||
|
||||
def mask_write_register(
|
||||
self,
|
||||
address=0x0000,
|
||||
and_mask=0xFFFF,
|
||||
or_mask=0x0000,
|
||||
slave=0,
|
||||
**kwargs,
|
||||
):
|
||||
"""Mask content of holding register at `address` with `and_mask` and `or_mask`.
|
||||
|
||||
:param address: Reference address of register
|
||||
:param and_mask: And Mask
|
||||
:param or_mask: OR Mask
|
||||
:param slave: Modbus slave slave ID
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
resp = super().mask_write_register( # pylint: disable=no-member
|
||||
address=address, and_mask=and_mask, or_mask=or_mask, slave=slave, **kwargs
|
||||
)
|
||||
if not resp.isError():
|
||||
return {
|
||||
"function_code": resp.function_code,
|
||||
"address": resp.address,
|
||||
"and mask": resp.and_mask,
|
||||
"or mask": resp.or_mask,
|
||||
}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=slave)
|
||||
|
||||
def read_device_information(self, read_code=None, object_id=0x00, **kwargs):
|
||||
"""Read the identification and additional information of remote slave.
|
||||
|
||||
:param read_code: Read Device ID code (0x01/0x02/0x03/0x04)
|
||||
:param object_id: Identification of the first object to obtain.
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReadDeviceInformationRequest(read_code, object_id, **kwargs)
|
||||
resp = self.execute(request) # pylint: disable=no-member
|
||||
if not resp.isError():
|
||||
return {
|
||||
"function_code": resp.function_code,
|
||||
"information": resp.information,
|
||||
"object count": resp.number_of_objects,
|
||||
"conformity": resp.conformity,
|
||||
"next object id": resp.next_object_id,
|
||||
"more follows": resp.more_follows,
|
||||
"space left": resp.space_left,
|
||||
}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=request.slave_id)
|
||||
|
||||
def report_slave_id(self, slave=0, **kwargs):
|
||||
"""Report information about remote slave ID.
|
||||
|
||||
:param slave: Modbus slave ID
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReportSlaveIdRequest(slave, **kwargs)
|
||||
resp = self.execute(request) # pylint: disable=no-member
|
||||
if not resp.isError():
|
||||
return {
|
||||
"function_code": resp.function_code,
|
||||
"identifier": resp.identifier.decode("cp1252"),
|
||||
"status": resp.status,
|
||||
"byte count": resp.byte_count,
|
||||
}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=slave)
|
||||
|
||||
def read_exception_status(self, slave=0, **kwargs):
|
||||
"""Read contents of eight Exception Status output in a remote device.
|
||||
|
||||
:param slave: Modbus slave ID
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReadExceptionStatusRequest(slave, **kwargs)
|
||||
resp = self.execute(request) # pylint: disable=no-member
|
||||
if not resp.isError():
|
||||
return {"function_code": resp.function_code, "status": resp.status}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=request.slave_id)
|
||||
|
||||
def get_com_event_counter(self, **kwargs):
|
||||
"""Read status word and an event count.
|
||||
|
||||
From the remote device's communication event counter.
|
||||
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = GetCommEventCounterRequest(**kwargs)
|
||||
resp = self.execute(request) # pylint: disable=no-member
|
||||
if not resp.isError():
|
||||
return {
|
||||
"function_code": resp.function_code,
|
||||
"status": resp.status,
|
||||
"count": resp.count,
|
||||
}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=request.slave_id)
|
||||
|
||||
def get_com_event_log(self, **kwargs):
|
||||
"""Read status word.
|
||||
|
||||
Event count, message count, and a field of event
|
||||
bytes from the remote device.
|
||||
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = GetCommEventLogRequest(**kwargs)
|
||||
resp = self.execute(request) # pylint: disable=no-member
|
||||
if not resp.isError():
|
||||
return {
|
||||
"function_code": resp.function_code,
|
||||
"status": resp.status,
|
||||
"message count": resp.message_count,
|
||||
"event count": resp.event_count,
|
||||
"events": resp.events,
|
||||
}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=request.slave_id)
|
||||
|
||||
def _execute_diagnostic_request(self, request):
|
||||
"""Execute diagnostic request."""
|
||||
resp = self.execute(request) # pylint: disable=no-member
|
||||
if not resp.isError():
|
||||
return {
|
||||
"function code": resp.function_code,
|
||||
"sub function code": resp.sub_function_code,
|
||||
"message": resp.message,
|
||||
}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=request.slave_id)
|
||||
|
||||
def return_query_data(self, message=0, **kwargs):
|
||||
"""Loop back data sent in response.
|
||||
|
||||
:param message: Message to be looped back
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReturnQueryDataRequest(message, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def restart_comm_option(self, toggle=False, **kwargs):
|
||||
"""Initialize and restart remote devices.
|
||||
|
||||
Serial interface and clear all of its communications event counters.
|
||||
|
||||
:param toggle: Toggle Status [ON(0xff00)/OFF(0x0000]
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = RestartCommunicationsOptionRequest(toggle, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_diagnostic_register(self, data=0, **kwargs):
|
||||
"""Read 16-bit diagnostic register.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReturnDiagnosticRegisterRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def change_ascii_input_delimiter(self, data=0, **kwargs):
|
||||
"""Change message delimiter for future requests.
|
||||
|
||||
:param data: New delimiter character
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ChangeAsciiInputDelimiterRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def force_listen_only_mode(self, data=0, **kwargs):
|
||||
"""Force addressed remote device to its Listen Only Mode.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ForceListenOnlyModeRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def clear_counters(self, data=0, **kwargs):
|
||||
"""Clear all counters and diag registers.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ClearCountersRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_bus_message_count(self, data=0, **kwargs):
|
||||
"""Return count of message detected on bus by remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReturnBusMessageCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_bus_com_error_count(self, data=0, **kwargs):
|
||||
"""Return count of CRC errors received by remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReturnBusCommunicationErrorCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_bus_exception_error_count(self, data=0, **kwargs):
|
||||
"""Return count of Modbus exceptions returned by remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReturnBusExceptionErrorCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_slave_message_count(self, data=0, **kwargs):
|
||||
"""Return count of messages addressed to remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReturnSlaveMessageCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_slave_no_response_count(self, data=0, **kwargs):
|
||||
"""Return count of No responses by remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReturnSlaveNoResponseCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_slave_no_ack_count(self, data=0, **kwargs):
|
||||
"""Return count of NO ACK exceptions sent by remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReturnSlaveNAKCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_slave_busy_count(self, data=0, **kwargs):
|
||||
"""Return count of server busy exceptions sent by remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReturnSlaveBusyCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_slave_bus_char_overrun_count(self, data=0, **kwargs):
|
||||
"""Return count of messages not handled.
|
||||
|
||||
By remote slave due to character overrun condition.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReturnSlaveBusCharacterOverrunCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_iop_overrun_count(self, data=0, **kwargs):
|
||||
"""Return count of iop overrun errors by remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReturnIopOverrunCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def clear_overrun_count(self, data=0, **kwargs):
|
||||
"""Clear over run counter.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ClearOverrunCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def get_clear_modbus_plus(self, data=0, **kwargs):
|
||||
"""Get/clear stats of remote modbus plus device.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = GetClearModbusPlusRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
|
||||
class ModbusSerialClient(ExtendedRequestSupport, _ModbusSerialClient):
|
||||
"""Modbus serial client."""
|
||||
|
||||
def __init__(self, framer, **kwargs):
|
||||
"""Initialize."""
|
||||
super().__init__(framer=framer, **kwargs)
|
||||
|
||||
def get_port(self):
|
||||
"""Get serial Port.
|
||||
|
||||
:return: Current Serial port
|
||||
"""
|
||||
return self.comm_params.port
|
||||
|
||||
def set_port(self, value):
|
||||
"""Set serial Port setter.
|
||||
|
||||
:param value: New port
|
||||
"""
|
||||
self.comm_params.port = value
|
||||
if self.is_socket_open():
|
||||
self.close()
|
||||
|
||||
def get_stopbits(self):
|
||||
"""Get number of stop bits.
|
||||
|
||||
:return: Current Stop bits
|
||||
"""
|
||||
return self.params.stopbits
|
||||
|
||||
def set_stopbits(self, value):
|
||||
"""Set stop bit.
|
||||
|
||||
:param value: Possible values (1, 1.5, 2)
|
||||
"""
|
||||
self.params.stopbits = float(value)
|
||||
if self.is_socket_open():
|
||||
self.close()
|
||||
|
||||
def get_bytesize(self):
|
||||
"""Get number of data bits.
|
||||
|
||||
:return: Current bytesize
|
||||
"""
|
||||
return self.comm_params.bytesize
|
||||
|
||||
def set_bytesize(self, value):
|
||||
"""Set Byte size.
|
||||
|
||||
:param value: Possible values (5, 6, 7, 8)
|
||||
|
||||
"""
|
||||
self.comm_params.bytesize = int(value)
|
||||
if self.is_socket_open():
|
||||
self.close()
|
||||
|
||||
def get_parity(self):
|
||||
"""Enable Parity Checking.
|
||||
|
||||
:return: Current parity setting
|
||||
"""
|
||||
return self.params.parity
|
||||
|
||||
def set_parity(self, value):
|
||||
"""Set parity Setter.
|
||||
|
||||
:param value: Possible values ("N", "E", "O", "M", "S")
|
||||
"""
|
||||
self.params.parity = value
|
||||
if self.is_socket_open():
|
||||
self.close()
|
||||
|
||||
def get_baudrate(self):
|
||||
"""Get serial Port baudrate.
|
||||
|
||||
:return: Current baudrate
|
||||
"""
|
||||
return self.comm_params.baudrate
|
||||
|
||||
def set_baudrate(self, value):
|
||||
"""Set baudrate setter.
|
||||
|
||||
:param value: <supported baudrate>
|
||||
"""
|
||||
self.comm_params.baudrate = int(value)
|
||||
if self.is_socket_open():
|
||||
self.close()
|
||||
|
||||
def get_timeout(self):
|
||||
"""Get serial Port Read timeout.
|
||||
|
||||
:return: Current read imeout.
|
||||
"""
|
||||
return self.comm_params.timeout_connect
|
||||
|
||||
def set_timeout(self, value):
|
||||
"""Read timeout setter.
|
||||
|
||||
:param value: Read Timeout in seconds
|
||||
"""
|
||||
self.comm_params.timeout_connect = float(value)
|
||||
if self.is_socket_open():
|
||||
self.close()
|
||||
|
||||
def get_serial_settings(self):
|
||||
"""Get Current Serial port settings.
|
||||
|
||||
:return: Current Serial settings as dict.
|
||||
"""
|
||||
return {
|
||||
"baudrate": self.comm_params.baudrate,
|
||||
"port": self.comm_params.port,
|
||||
"parity": self.comm_params.parity,
|
||||
"stopbits": self.comm_params.stopbits,
|
||||
"bytesize": self.comm_params.bytesize,
|
||||
"read timeout": self.comm_params.timeout_connect,
|
||||
"t1.5": self.inter_char_timeout,
|
||||
"t3.5": self.silent_interval,
|
||||
}
|
||||
|
||||
|
||||
class ModbusTcpClient(ExtendedRequestSupport, _ModbusTcpClient):
|
||||
"""TCP client."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize."""
|
||||
super().__init__(**kwargs)
|
||||
1
env/lib/python3.11/site-packages/pymodbus/repl/server/__init__.py
vendored
Normal file
1
env/lib/python3.11/site-packages/pymodbus/repl/server/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
"""Repl server."""
|
||||
BIN
env/lib/python3.11/site-packages/pymodbus/repl/server/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/repl/server/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/repl/server/__pycache__/cli.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/repl/server/__pycache__/cli.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/repl/server/__pycache__/main.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/repl/server/__pycache__/main.cpython-311.pyc
vendored
Normal file
Binary file not shown.
211
env/lib/python3.11/site-packages/pymodbus/repl/server/cli.py
vendored
Normal file
211
env/lib/python3.11/site-packages/pymodbus/repl/server/cli.py
vendored
Normal file
@@ -0,0 +1,211 @@
|
||||
"""Repl server cli."""
|
||||
import shutil
|
||||
|
||||
import click
|
||||
from prompt_toolkit import PromptSession, print_formatted_text
|
||||
from prompt_toolkit.completion import NestedCompleter
|
||||
from prompt_toolkit.formatted_text import HTML
|
||||
from prompt_toolkit.shortcuts import clear
|
||||
from prompt_toolkit.shortcuts.progress_bar import formatters
|
||||
from prompt_toolkit.styles import Style
|
||||
|
||||
|
||||
TITLE = r"""
|
||||
__________ .______. _________
|
||||
\______ \___.__. _____ ____ __| _/\_ |__ __ __ ______ / _____/ ______________ __ ___________
|
||||
| ___< | |/ \ / _ \ / __ | | __ \| | \/ ___/ \_____ \_/ __ \_ __ \ \/ // __ \_ __ \\
|
||||
| | \___ | Y Y ( <_> ) /_/ | | \_\ \ | /\___ \ / \ ___/| | \/\ /\ ___/| | \/
|
||||
|____| / ____|__|_| /\____/\____ | |___ /____//____ > /_______ /\___ >__| \_/ \___ >__|
|
||||
\/ \/ \/ \/ \/ \/ \/ \/
|
||||
"""
|
||||
|
||||
|
||||
SMALL_TITLE = "Pymodbus server..."
|
||||
BOTTOM_TOOLBAR = HTML(
|
||||
'(MODBUS SERVER) <b><style bg="ansired">Press Ctrl+C or '
|
||||
'type "exit" to quit</style></b> Type "help" '
|
||||
"for list of available commands"
|
||||
)
|
||||
COMMAND_ARGS = ["response_type", "error_code", "delay_by", "clear_after", "data_len"]
|
||||
RESPONSE_TYPES = ["normal", "error", "delayed", "empty", "stray"]
|
||||
COMMANDS = {
|
||||
"manipulator": {
|
||||
"response_type": None,
|
||||
"error_code": None,
|
||||
"delay_by": None,
|
||||
"clear_after": None,
|
||||
},
|
||||
"exit": None,
|
||||
"help": None,
|
||||
"clear": None,
|
||||
}
|
||||
USAGE = (
|
||||
"manipulator response_type=|normal|error|delayed|empty|stray \n"
|
||||
"\tAdditional parameters\n"
|
||||
"\t\terror_code=<int> \n\t\tdelay_by=<in seconds> \n\t\t"
|
||||
"clear_after=<clear after n messages int>"
|
||||
"\n\t\tdata_len=<length of stray data (int)>\n"
|
||||
"\n\tExample usage: \n\t"
|
||||
"1. Send error response 3 for 4 requests\n\t"
|
||||
" <ansiblue>manipulator response_type=error error_code=3 clear_after=4</ansiblue>\n\t"
|
||||
"2. Delay outgoing response by 5 seconds indefinitely\n\t"
|
||||
" <ansiblue>manipulator response_type=delayed delay_by=5</ansiblue>\n\t"
|
||||
"3. Send empty response\n\t"
|
||||
" <ansiblue>manipulator response_type=empty</ansiblue>\n\t"
|
||||
"4. Send stray response of length 12 and revert to normal after 2 responses\n\t"
|
||||
" <ansiblue>manipulator response_type=stray data_len=11 clear_after=2</ansiblue>\n\t"
|
||||
"5. To disable response manipulation\n\t"
|
||||
" <ansiblue>manipulator response_type=normal</ansiblue>"
|
||||
)
|
||||
COMMAND_HELPS = {
|
||||
"manipulator": f"Manipulate response from server.\nUsage: {USAGE}",
|
||||
"clear": "Clears screen",
|
||||
}
|
||||
|
||||
|
||||
STYLE = Style.from_dict({"": "cyan"})
|
||||
CUSTOM_FORMATTERS = [
|
||||
formatters.Label(suffix=": "),
|
||||
formatters.Bar(start="|", end="|", sym_a="#", sym_b="#", sym_c="-"),
|
||||
formatters.Text(" "),
|
||||
formatters.Text(" "),
|
||||
formatters.TimeElapsed(),
|
||||
formatters.Text(" "),
|
||||
]
|
||||
|
||||
|
||||
def info(message):
|
||||
"""Show info."""
|
||||
if not isinstance(message, str):
|
||||
message = str(message)
|
||||
click.secho(message, fg="green")
|
||||
|
||||
|
||||
def warning(message):
|
||||
"""Show warning."""
|
||||
click.secho(str(message), fg="yellow")
|
||||
|
||||
|
||||
def error(message):
|
||||
"""Show error."""
|
||||
click.secho(str(message), fg="red")
|
||||
|
||||
|
||||
def get_terminal_width():
|
||||
"""Get terminal width."""
|
||||
return shutil.get_terminal_size()[0]
|
||||
|
||||
|
||||
def print_help():
|
||||
"""Print help."""
|
||||
print_formatted_text(HTML("<u>Available commands:</u>"))
|
||||
for cmd, hlp in sorted(COMMAND_HELPS.items()):
|
||||
print_formatted_text(
|
||||
HTML(f"<skyblue>{cmd:45s}</skyblue><seagreen>{hlp:100s}</seagreen>")
|
||||
)
|
||||
|
||||
|
||||
def print_title():
|
||||
"""Print title - large if there are sufficient columns, otherwise small."""
|
||||
col = get_terminal_width()
|
||||
max_len = max( # pylint: disable=consider-using-generator
|
||||
[len(t) for t in TITLE.split("\n")]
|
||||
)
|
||||
if col > max_len:
|
||||
info(TITLE)
|
||||
else:
|
||||
print_formatted_text(
|
||||
HTML(f'<u><b><style color="green">{SMALL_TITLE}</style></b></u>')
|
||||
)
|
||||
|
||||
|
||||
async def interactive_shell(server):
|
||||
"""Run CLI interactive shell."""
|
||||
print_title()
|
||||
info("")
|
||||
completer = NestedCompleter.from_nested_dict(COMMANDS)
|
||||
session = PromptSession(
|
||||
"SERVER > ", completer=completer, bottom_toolbar=BOTTOM_TOOLBAR
|
||||
)
|
||||
|
||||
# Run echo loop. Read text from stdin, and reply it back.
|
||||
while True:
|
||||
try:
|
||||
result = await session.prompt_async()
|
||||
if result == "exit":
|
||||
await server.web_app.shutdown()
|
||||
break
|
||||
if result == "help":
|
||||
print_help()
|
||||
continue
|
||||
if result == "clear":
|
||||
clear()
|
||||
continue
|
||||
if command := result.split():
|
||||
if command[0] not in COMMANDS:
|
||||
warning(f"Invalid command or invalid usage of command - {command}")
|
||||
continue
|
||||
if len(command) == 1:
|
||||
warning(f'Usage: "{USAGE}"')
|
||||
else:
|
||||
val_dict = _process_args(command[1:])
|
||||
if val_dict: # pylint: disable=consider-using-assignment-expr
|
||||
server.update_manipulator_config(val_dict)
|
||||
# server.manipulator_config = val_dict
|
||||
# result = await run_command(tester, *command)
|
||||
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
return
|
||||
|
||||
|
||||
def _process_args(args) -> dict:
|
||||
"""Process arguments passed to CLI."""
|
||||
skip_next = False
|
||||
val_dict = {}
|
||||
for index, arg in enumerate(args):
|
||||
if skip_next:
|
||||
skip_next = False
|
||||
continue
|
||||
if "=" in arg:
|
||||
arg, value = arg.split("=")
|
||||
elif arg in COMMAND_ARGS:
|
||||
try:
|
||||
value = args[index + 1]
|
||||
skip_next = True
|
||||
except IndexError:
|
||||
error(f"Missing value for argument - {arg}")
|
||||
warning('Usage: "{USAGE}"')
|
||||
break
|
||||
if arg == "response_type":
|
||||
if value not in RESPONSE_TYPES:
|
||||
warning(f"Invalid response type request - {value}")
|
||||
warning(f"Choose from {RESPONSE_TYPES}")
|
||||
continue
|
||||
elif arg in { # pylint: disable=confusing-consecutive-elif
|
||||
"error_code",
|
||||
"delay_by",
|
||||
"clear_after",
|
||||
"data_len",
|
||||
}:
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
warning(f"Expected integer value for {arg}")
|
||||
continue
|
||||
val_dict[arg] = value
|
||||
return val_dict
|
||||
|
||||
|
||||
async def main(server):
|
||||
"""Run main."""
|
||||
# with patch_stdout():
|
||||
try:
|
||||
await interactive_shell(server)
|
||||
finally:
|
||||
pass
|
||||
warning("Bye Bye!!!")
|
||||
|
||||
|
||||
async def run_repl(server):
|
||||
"""Run repl server."""
|
||||
await main(server)
|
||||
209
env/lib/python3.11/site-packages/pymodbus/repl/server/main.py
vendored
Normal file
209
env/lib/python3.11/site-packages/pymodbus/repl/server/main.py
vendored
Normal file
@@ -0,0 +1,209 @@
|
||||
"""Repl server main."""
|
||||
import asyncio
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
import typer
|
||||
|
||||
from pymodbus import pymodbus_apply_logging_config
|
||||
from pymodbus.framer.socket_framer import ModbusSocketFramer
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.repl.server.cli import run_repl
|
||||
from pymodbus.server.reactive.default_config import DEFAULT_CONFIG
|
||||
from pymodbus.server.reactive.main import (
|
||||
DEFAULT_FRAMER,
|
||||
ReactiveServer,
|
||||
)
|
||||
|
||||
|
||||
CANCELLED_ERROR = asyncio.exceptions.CancelledError
|
||||
CONTEXT_SETTING = {"allow_extra_args": True, "ignore_unknown_options": True}
|
||||
|
||||
|
||||
# TBD class ModbusServerConfig:
|
||||
|
||||
|
||||
class ModbusServerTypes(str, Enum):
|
||||
"""Server types."""
|
||||
|
||||
# ["tcp", "serial", "tls", "udp"]
|
||||
tcp = "tcp" # pylint: disable=invalid-name
|
||||
serial = "serial" # pylint: disable=invalid-name
|
||||
tls = "tls" # pylint: disable=invalid-name
|
||||
udp = "udp" # pylint: disable=invalid-name
|
||||
|
||||
|
||||
class ModbusFramerTypes(str, Enum):
|
||||
"""Framer types."""
|
||||
|
||||
# ["socket", "rtu", "tls", "ascii", "binary"]
|
||||
socket = "socket" # pylint: disable=invalid-name
|
||||
rtu = "rtu" # pylint: disable=invalid-name
|
||||
tls = "tls" # pylint: disable=invalid-name
|
||||
ascii = "ascii" # pylint: disable=invalid-name
|
||||
binary = "binary" # pylint: disable=invalid-name
|
||||
|
||||
|
||||
def _completer(incomplete: str, valid_values: List[str]) -> List[str]:
|
||||
"""Complete value."""
|
||||
completion = []
|
||||
for name in valid_values:
|
||||
if name.startswith(incomplete):
|
||||
completion.append(name)
|
||||
return completion
|
||||
|
||||
|
||||
def framers(incomplete: str) -> List[str]:
|
||||
"""Return an autocompleted list of supported clouds."""
|
||||
_framers = ["socket", "rtu", "tls", "ascii", "binary"]
|
||||
return _completer(incomplete, _framers)
|
||||
|
||||
|
||||
def servers(incomplete: str) -> List[str]:
|
||||
"""Return an autocompleted list of supported clouds."""
|
||||
_servers = ["tcp", "serial", "tls", "udp"]
|
||||
return _completer(incomplete, _servers)
|
||||
|
||||
|
||||
def process_extra_args(extra_args: List[str], modbus_config: dict) -> dict:
|
||||
"""Process extra args passed to server."""
|
||||
options_stripped = [x.strip().replace("--", "") for x in extra_args[::2]]
|
||||
extra_args_dict = dict(list(zip(options_stripped, extra_args[1::2])))
|
||||
for option, value in extra_args_dict.items():
|
||||
if option in modbus_config:
|
||||
try:
|
||||
modbus_config[option] = type(modbus_config[option])(value)
|
||||
except ValueError as err:
|
||||
Log.error(
|
||||
"Error parsing extra arg {} with value '{}'. {}", option, value, err
|
||||
)
|
||||
sys.exit(1)
|
||||
return modbus_config
|
||||
|
||||
|
||||
app = typer.Typer(
|
||||
no_args_is_help=True,
|
||||
context_settings=CONTEXT_SETTING,
|
||||
help="Reactive Modbus server",
|
||||
)
|
||||
|
||||
|
||||
@app.callback()
|
||||
def server(
|
||||
ctx: typer.Context,
|
||||
host: str = typer.Option("localhost", "--host", help="Host address"),
|
||||
web_port: int = typer.Option(8080, "--web-port", help="Web app port"),
|
||||
broadcast_support: bool = typer.Option(
|
||||
False, "-b", help="Support broadcast messages"
|
||||
),
|
||||
repl: bool = typer.Option(True, help="Enable/Disable repl for server"),
|
||||
verbose: bool = typer.Option(
|
||||
False, help="Run with debug logs enabled for pymodbus"
|
||||
),
|
||||
):
|
||||
"""Run server code."""
|
||||
log_level = logging.DEBUG if verbose else logging.ERROR
|
||||
pymodbus_apply_logging_config(log_level)
|
||||
|
||||
ctx.obj = {
|
||||
"repl": repl,
|
||||
"host": host,
|
||||
"web_port": web_port,
|
||||
"broadcast_enable": broadcast_support,
|
||||
}
|
||||
|
||||
|
||||
@app.command("run", context_settings=CONTEXT_SETTING)
|
||||
def run(
|
||||
ctx: typer.Context,
|
||||
modbus_server: str = typer.Option(
|
||||
ModbusServerTypes.tcp.value,
|
||||
"--modbus-server",
|
||||
"-s",
|
||||
case_sensitive=False,
|
||||
autocompletion=servers,
|
||||
help="Modbus Server",
|
||||
),
|
||||
modbus_framer: str = typer.Option(
|
||||
ModbusFramerTypes.socket.value,
|
||||
"--framer",
|
||||
"-f",
|
||||
case_sensitive=False,
|
||||
autocompletion=framers,
|
||||
help="Modbus framer to use",
|
||||
),
|
||||
modbus_port: str = typer.Option("5020", "--modbus-port", "-p", help="Modbus port"),
|
||||
modbus_slave_id: List[int] = typer.Option(
|
||||
[1], "--slave-id", "-u", help="Supported Modbus slave id's"
|
||||
),
|
||||
modbus_config_path: Path = typer.Option(
|
||||
None, help="Path to additional modbus server config"
|
||||
),
|
||||
randomize: int = typer.Option(
|
||||
0,
|
||||
"--random",
|
||||
"-r",
|
||||
help="Randomize every `r` reads. 0=never, 1=always,2=every-second-read"
|
||||
", and so on. Applicable IR and DI.",
|
||||
),
|
||||
change_rate: int = typer.Option(
|
||||
0,
|
||||
"--change-rate",
|
||||
"-c",
|
||||
help="Rate in % registers to change. 0=none, 100=all, 12=12% of registers"
|
||||
", and so on. Applicable IR and DI.",
|
||||
),
|
||||
):
|
||||
"""Run Reactive Modbus server.
|
||||
|
||||
Exposing REST endpoint for response manipulation.
|
||||
"""
|
||||
repl = ctx.obj.pop("repl")
|
||||
# TBD extra_args = ctx.args
|
||||
web_app_config = ctx.obj
|
||||
loop = asyncio.get_event_loop()
|
||||
framer = DEFAULT_FRAMER.get(modbus_framer, ModbusSocketFramer)
|
||||
if modbus_config_path:
|
||||
with open(modbus_config_path, encoding="utf-8") as my_file:
|
||||
modbus_config = json.load(my_file)
|
||||
else:
|
||||
modbus_config = DEFAULT_CONFIG
|
||||
|
||||
extra_args = ctx.args
|
||||
data_block_settings = modbus_config.pop("data_block_settings", {})
|
||||
modbus_config = modbus_config.get(modbus_server, {})
|
||||
modbus_config = process_extra_args(extra_args, modbus_config)
|
||||
|
||||
modbus_config["randomize"] = randomize
|
||||
modbus_config["change_rate"] = change_rate
|
||||
|
||||
async def _wrapper():
|
||||
app = ReactiveServer.factory(
|
||||
modbus_server,
|
||||
framer,
|
||||
modbus_port=modbus_port,
|
||||
slave=modbus_slave_id,
|
||||
single=False,
|
||||
data_block_settings=data_block_settings,
|
||||
**web_app_config,
|
||||
**modbus_config,
|
||||
)
|
||||
await app.run_async(repl)
|
||||
return app
|
||||
|
||||
app = loop.run_until_complete(_wrapper())
|
||||
if repl:
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
loop.run_until_complete(run_repl(app))
|
||||
|
||||
else:
|
||||
loop.run_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
42
env/lib/python3.11/site-packages/pymodbus/server/__init__.py
vendored
Normal file
42
env/lib/python3.11/site-packages/pymodbus/server/__init__.py
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
"""Server.
|
||||
|
||||
import external classes, to make them easier to use:
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
"get_simulator_commandline",
|
||||
"ModbusSerialServer",
|
||||
"ModbusSimulatorServer",
|
||||
"ModbusTcpServer",
|
||||
"ModbusTlsServer",
|
||||
"ModbusUdpServer",
|
||||
"ServerAsyncStop",
|
||||
"ServerStop",
|
||||
"StartAsyncSerialServer",
|
||||
"StartAsyncTcpServer",
|
||||
"StartAsyncTlsServer",
|
||||
"StartAsyncUdpServer",
|
||||
"StartSerialServer",
|
||||
"StartTcpServer",
|
||||
"StartTlsServer",
|
||||
"StartUdpServer",
|
||||
]
|
||||
|
||||
from pymodbus.server.async_io import (
|
||||
ModbusSerialServer,
|
||||
ModbusTcpServer,
|
||||
ModbusTlsServer,
|
||||
ModbusUdpServer,
|
||||
ServerAsyncStop,
|
||||
ServerStop,
|
||||
StartAsyncSerialServer,
|
||||
StartAsyncTcpServer,
|
||||
StartAsyncTlsServer,
|
||||
StartAsyncUdpServer,
|
||||
StartSerialServer,
|
||||
StartTcpServer,
|
||||
StartTlsServer,
|
||||
StartUdpServer,
|
||||
)
|
||||
from pymodbus.server.simulator.http_server import ModbusSimulatorServer
|
||||
from pymodbus.server.simulator.main import get_commandline as get_simulator_commandline
|
||||
BIN
env/lib/python3.11/site-packages/pymodbus/server/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/server/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/server/__pycache__/async_io.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/server/__pycache__/async_io.cpython-311.pyc
vendored
Normal file
Binary file not shown.
742
env/lib/python3.11/site-packages/pymodbus/server/async_io.py
vendored
Normal file
742
env/lib/python3.11/site-packages/pymodbus/server/async_io.py
vendored
Normal file
@@ -0,0 +1,742 @@
|
||||
"""Implementation of a Threaded Modbus Server."""
|
||||
# pylint: disable=missing-type-doc
|
||||
import asyncio
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
from contextlib import suppress
|
||||
from typing import Union
|
||||
|
||||
from pymodbus.datastore import ModbusServerContext
|
||||
from pymodbus.device import ModbusControlBlock, ModbusDeviceIdentification
|
||||
from pymodbus.exceptions import NoSuchSlaveException
|
||||
from pymodbus.factory import ServerDecoder
|
||||
from pymodbus.framer import ModbusFramer
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.pdu import ModbusExceptions as merror
|
||||
from pymodbus.transaction import (
|
||||
ModbusAsciiFramer,
|
||||
ModbusRtuFramer,
|
||||
ModbusSocketFramer,
|
||||
ModbusTlsFramer,
|
||||
)
|
||||
from pymodbus.transport import CommParams, CommType, ModbusProtocol
|
||||
|
||||
|
||||
with suppress(ImportError):
|
||||
pass
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Protocol Handlers
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class ModbusServerRequestHandler(ModbusProtocol):
|
||||
"""Implements modbus slave wire protocol.
|
||||
|
||||
This uses the asyncio.Protocol to implement the server protocol.
|
||||
|
||||
When a connection is established, a callback is called.
|
||||
This callback will setup the connection and
|
||||
create and schedule an asyncio.Task and assign it to running_task.
|
||||
"""
|
||||
|
||||
def __init__(self, owner):
|
||||
"""Initialize."""
|
||||
params = CommParams(
|
||||
comm_name="server",
|
||||
comm_type=owner.comm_params.comm_type,
|
||||
reconnect_delay=0.0,
|
||||
reconnect_delay_max=0.0,
|
||||
timeout_connect=0.0,
|
||||
host=owner.comm_params.source_address[0],
|
||||
port=owner.comm_params.source_address[1],
|
||||
)
|
||||
super().__init__(params, False)
|
||||
self.server = owner
|
||||
self.running = False
|
||||
self.receive_queue = asyncio.Queue()
|
||||
self.handler_task = None # coroutine to be run on asyncio loop
|
||||
self.framer: ModbusFramer = None
|
||||
|
||||
def _log_exception(self):
|
||||
"""Show log exception."""
|
||||
Log.debug(
|
||||
"Handler for stream [{}] has been canceled", self.comm_params.comm_name
|
||||
)
|
||||
|
||||
def callback_connected(self) -> None:
|
||||
"""Call when connection is succcesfull."""
|
||||
try:
|
||||
self.running = True
|
||||
self.framer = self.server.framer(
|
||||
self.server.decoder,
|
||||
client=None,
|
||||
)
|
||||
|
||||
# schedule the connection handler on the event loop
|
||||
self.handler_task = asyncio.create_task(self.handle())
|
||||
except Exception as exc: # pragma: no cover pylint: disable=broad-except
|
||||
Log.error(
|
||||
"Server callback_connected exception: {}; {}",
|
||||
exc,
|
||||
traceback.format_exc(),
|
||||
)
|
||||
|
||||
def callback_disconnected(self, call_exc: Exception) -> None:
|
||||
"""Call when connection is lost."""
|
||||
try:
|
||||
if self.handler_task:
|
||||
self.handler_task.cancel()
|
||||
if hasattr(self.server, "on_connection_lost"):
|
||||
self.server.on_connection_lost()
|
||||
if call_exc is None:
|
||||
self._log_exception()
|
||||
else:
|
||||
Log.debug(
|
||||
"Client Disconnection {} due to {}",
|
||||
self.comm_params.comm_name,
|
||||
call_exc,
|
||||
)
|
||||
self.running = False
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
Log.error(
|
||||
"Datastore unable to fulfill request: {}; {}",
|
||||
exc,
|
||||
traceback.format_exc(),
|
||||
)
|
||||
|
||||
async def inner_handle(self):
|
||||
"""Handle handler."""
|
||||
slaves = self.server.context.slaves()
|
||||
# this is an asyncio.Queue await, it will never fail
|
||||
data = await self._recv_()
|
||||
if isinstance(data, tuple):
|
||||
# addr is populated when talking over UDP
|
||||
data, *addr = data
|
||||
else:
|
||||
addr = (None,) # empty tuple
|
||||
|
||||
# if broadcast is enabled make sure to
|
||||
# process requests to address 0
|
||||
if self.server.broadcast_enable: # pragma: no cover
|
||||
if 0 not in slaves:
|
||||
slaves.append(0)
|
||||
|
||||
Log.debug("Handling data: {}", data, ":hex")
|
||||
|
||||
single = self.server.context.single
|
||||
self.framer.processIncomingPacket(
|
||||
data=data,
|
||||
callback=lambda x: self.execute(x, *addr),
|
||||
slave=slaves,
|
||||
single=single,
|
||||
)
|
||||
|
||||
async def handle(self):
|
||||
"""Return Asyncio coroutine which represents a single conversation.
|
||||
|
||||
between the modbus slave and master
|
||||
|
||||
Once the client connection is established, the data chunks will be
|
||||
fed to this coroutine via the asyncio.Queue object which is fed by
|
||||
the ModbusServerRequestHandler class's callback Future.
|
||||
|
||||
This callback future gets data from either
|
||||
|
||||
This function will execute without blocking in the while-loop and
|
||||
yield to the asyncio event loop when the frame is exhausted.
|
||||
As a result, multiple clients can be interleaved without any
|
||||
interference between them.
|
||||
"""
|
||||
reset_frame = False
|
||||
while self.running:
|
||||
try:
|
||||
await self.inner_handle()
|
||||
except asyncio.CancelledError:
|
||||
# catch and ignore cancellation errors
|
||||
if self.running:
|
||||
self._log_exception()
|
||||
self.running = False
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
# force TCP socket termination as processIncomingPacket
|
||||
# should handle application layer errors
|
||||
# for UDP sockets, simply reset the frame
|
||||
if isinstance(self, ModbusServerRequestHandler):
|
||||
Log.error(
|
||||
'Unknown exception "{}" on stream {} forcing disconnect',
|
||||
exc,
|
||||
self.comm_params.comm_name,
|
||||
)
|
||||
self.transport_close()
|
||||
self.callback_disconnected(exc)
|
||||
else:
|
||||
Log.error("Unknown error occurred {}", exc)
|
||||
reset_frame = True # graceful recovery
|
||||
finally:
|
||||
if reset_frame:
|
||||
self.framer.resetFrame()
|
||||
reset_frame = False
|
||||
|
||||
def execute(self, request, *addr):
|
||||
"""Call with the resulting message.
|
||||
|
||||
:param request: The decoded request message
|
||||
:param addr: the address
|
||||
"""
|
||||
if self.server.request_tracer:
|
||||
self.server.request_tracer(request, *addr)
|
||||
|
||||
broadcast = False
|
||||
try:
|
||||
if self.server.broadcast_enable and not request.slave_id:
|
||||
broadcast = True
|
||||
# if broadcasting then execute on all slave contexts,
|
||||
# note response will be ignored
|
||||
for slave_id in self.server.context.slaves():
|
||||
response = request.execute(self.server.context[slave_id])
|
||||
else:
|
||||
context = self.server.context[request.slave_id]
|
||||
response = request.execute(context)
|
||||
except NoSuchSlaveException:
|
||||
Log.error("requested slave does not exist: {}", request.slave_id)
|
||||
if self.server.ignore_missing_slaves:
|
||||
return # the client will simply timeout waiting for a response
|
||||
response = request.doException(merror.GatewayNoResponse)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
Log.error(
|
||||
"Datastore unable to fulfill request: {}; {}",
|
||||
exc,
|
||||
traceback.format_exc(),
|
||||
)
|
||||
response = request.doException(merror.SlaveFailure)
|
||||
# no response when broadcasting
|
||||
if not broadcast:
|
||||
response.transaction_id = request.transaction_id
|
||||
response.slave_id = request.slave_id
|
||||
skip_encoding = False
|
||||
if self.server.response_manipulator:
|
||||
response, skip_encoding = self.server.response_manipulator(response)
|
||||
self.send(response, *addr, skip_encoding=skip_encoding)
|
||||
|
||||
def send(self, message, addr, **kwargs):
|
||||
"""Send message."""
|
||||
if kwargs.get("skip_encoding", False):
|
||||
self.transport_send(message, addr=addr)
|
||||
elif message.should_respond:
|
||||
pdu = self.framer.buildPacket(message)
|
||||
self.transport_send(pdu, addr=addr)
|
||||
else:
|
||||
Log.debug("Skipping sending response!!")
|
||||
|
||||
async def _recv_(self): # pragma: no cover
|
||||
"""Receive data from the network."""
|
||||
try:
|
||||
result = await self.receive_queue.get()
|
||||
except RuntimeError:
|
||||
Log.error("Event loop is closed")
|
||||
result = None
|
||||
return result
|
||||
|
||||
def callback_data(self, data: bytes, addr: tuple = None) -> int:
|
||||
"""Handle received data."""
|
||||
if addr:
|
||||
self.receive_queue.put_nowait((data, addr))
|
||||
else:
|
||||
self.receive_queue.put_nowait(data)
|
||||
return len(data)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Server Implementations
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class ModbusBaseServer(ModbusProtocol):
|
||||
"""Common functionality for all server classes"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
params: CommParams,
|
||||
context,
|
||||
ignore_missing_slaves,
|
||||
broadcast_enable,
|
||||
response_manipulator,
|
||||
request_tracer,
|
||||
identity,
|
||||
framer,
|
||||
) -> None:
|
||||
"""Initialize base server."""
|
||||
super().__init__(
|
||||
params,
|
||||
True,
|
||||
)
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.decoder = ServerDecoder()
|
||||
self.context = context or ModbusServerContext()
|
||||
self.control = ModbusControlBlock()
|
||||
self.ignore_missing_slaves = ignore_missing_slaves
|
||||
self.broadcast_enable = broadcast_enable
|
||||
self.response_manipulator = response_manipulator
|
||||
self.request_tracer = request_tracer
|
||||
self.handle_local_echo = False
|
||||
if isinstance(identity, ModbusDeviceIdentification):
|
||||
self.control.Identity.update(identity)
|
||||
self.framer = framer
|
||||
self.serving: asyncio.Future = asyncio.Future()
|
||||
|
||||
def callback_new_connection(self):
|
||||
"""Handle incoming connect."""
|
||||
return ModbusServerRequestHandler(self)
|
||||
|
||||
async def shutdown(self):
|
||||
"""Shutdown server."""
|
||||
await self.server_close()
|
||||
|
||||
async def server_close(self):
|
||||
"""Close server."""
|
||||
if not self.serving.done():
|
||||
self.serving.set_result(True)
|
||||
self.transport_close()
|
||||
|
||||
async def serve_forever(self):
|
||||
"""Start endless loop."""
|
||||
if self.transport:
|
||||
raise RuntimeError(
|
||||
"Can't call serve_forever on an already running server object"
|
||||
)
|
||||
await self.transport_listen()
|
||||
Log.info("Server listening.")
|
||||
await self.serving
|
||||
Log.info("Server graceful shutdown.")
|
||||
|
||||
|
||||
class ModbusTcpServer(ModbusBaseServer):
|
||||
"""A modbus threaded tcp socket server.
|
||||
|
||||
We inherit and overload the socket server so that we
|
||||
can control the client threads as well as have a single
|
||||
server context instance.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context,
|
||||
framer=ModbusSocketFramer,
|
||||
identity=None,
|
||||
address=("", 502),
|
||||
ignore_missing_slaves=False,
|
||||
broadcast_enable=False,
|
||||
response_manipulator=None,
|
||||
request_tracer=None,
|
||||
):
|
||||
"""Initialize the socket server.
|
||||
|
||||
If the identify structure is not passed in, the ModbusControlBlock
|
||||
uses its own empty structure.
|
||||
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param framer: The framer strategy to use
|
||||
:param identity: An optional identify structure
|
||||
:param address: An optional (interface, port) to bind to.
|
||||
:param ignore_missing_slaves: True to not send errors on a request
|
||||
to a missing slave
|
||||
:param broadcast_enable: True to treat slave_id 0 as broadcast address,
|
||||
False to treat 0 as any other slave_id
|
||||
:param response_manipulator: Callback method for manipulating the
|
||||
response
|
||||
:param request_tracer: Callback method for tracing
|
||||
"""
|
||||
params = getattr(
|
||||
self,
|
||||
"tls_setup",
|
||||
CommParams(
|
||||
comm_type=CommType.TCP,
|
||||
comm_name="server_listener",
|
||||
reconnect_delay=0.0,
|
||||
reconnect_delay_max=0.0,
|
||||
timeout_connect=0.0,
|
||||
),
|
||||
)
|
||||
params.source_address = address
|
||||
super().__init__(
|
||||
params,
|
||||
context,
|
||||
ignore_missing_slaves,
|
||||
broadcast_enable,
|
||||
response_manipulator,
|
||||
request_tracer,
|
||||
identity,
|
||||
framer,
|
||||
)
|
||||
|
||||
|
||||
class ModbusTlsServer(ModbusTcpServer):
|
||||
"""A modbus threaded tls socket server.
|
||||
|
||||
We inherit and overload the socket server so that we
|
||||
can control the client threads as well as have a single
|
||||
server context instance.
|
||||
"""
|
||||
|
||||
def __init__( # pylint: disable=too-many-arguments
|
||||
self,
|
||||
context,
|
||||
framer=ModbusTlsFramer,
|
||||
identity=None,
|
||||
address=("", 502),
|
||||
sslctx=None,
|
||||
certfile=None,
|
||||
keyfile=None,
|
||||
password=None,
|
||||
ignore_missing_slaves=False,
|
||||
broadcast_enable=False,
|
||||
response_manipulator=None,
|
||||
request_tracer=None,
|
||||
):
|
||||
"""Overloaded initializer for the socket server.
|
||||
|
||||
If the identify structure is not passed in, the ModbusControlBlock
|
||||
uses its own empty structure.
|
||||
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param framer: The framer strategy to use
|
||||
:param identity: An optional identify structure
|
||||
:param address: An optional (interface, port) to bind to.
|
||||
:param sslctx: The SSLContext to use for TLS (default None and auto
|
||||
create)
|
||||
:param certfile: The cert file path for TLS (used if sslctx is None)
|
||||
:param keyfile: The key file path for TLS (used if sslctx is None)
|
||||
:param password: The password for for decrypting the private key file
|
||||
:param ignore_missing_slaves: True to not send errors on a request
|
||||
to a missing slave
|
||||
:param broadcast_enable: True to treat slave_id 0 as broadcast address,
|
||||
False to treat 0 as any other slave_id
|
||||
:param response_manipulator: Callback method for
|
||||
manipulating the response
|
||||
"""
|
||||
self.tls_setup = CommParams(
|
||||
comm_type=CommType.TLS,
|
||||
comm_name="server_listener",
|
||||
reconnect_delay=0.0,
|
||||
reconnect_delay_max=0.0,
|
||||
timeout_connect=0.0,
|
||||
sslctx=CommParams.generate_ssl(
|
||||
True, certfile, keyfile, password, sslctx=sslctx
|
||||
),
|
||||
)
|
||||
super().__init__(
|
||||
context,
|
||||
framer=framer,
|
||||
identity=identity,
|
||||
address=address,
|
||||
ignore_missing_slaves=ignore_missing_slaves,
|
||||
broadcast_enable=broadcast_enable,
|
||||
response_manipulator=response_manipulator,
|
||||
request_tracer=request_tracer,
|
||||
)
|
||||
|
||||
|
||||
class ModbusUdpServer(ModbusBaseServer):
|
||||
"""A modbus threaded udp socket server.
|
||||
|
||||
We inherit and overload the socket server so that we
|
||||
can control the client threads as well as have a single
|
||||
server context instance.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context,
|
||||
framer=ModbusSocketFramer,
|
||||
identity=None,
|
||||
address=("", 502),
|
||||
ignore_missing_slaves=False,
|
||||
broadcast_enable=False,
|
||||
response_manipulator=None,
|
||||
request_tracer=None,
|
||||
):
|
||||
"""Overloaded initializer for the socket server.
|
||||
|
||||
If the identify structure is not passed in, the ModbusControlBlock
|
||||
uses its own empty structure.
|
||||
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param framer: The framer strategy to use
|
||||
:param identity: An optional identify structure
|
||||
:param address: An optional (interface, port) to bind to.
|
||||
:param ignore_missing_slaves: True to not send errors on a request
|
||||
to a missing slave
|
||||
:param broadcast_enable: True to treat slave_id 0 as broadcast address,
|
||||
False to treat 0 as any other slave_id
|
||||
:param response_manipulator: Callback method for
|
||||
manipulating the response
|
||||
:param request_tracer: Callback method for tracing
|
||||
"""
|
||||
# ----------------
|
||||
super().__init__(
|
||||
CommParams(
|
||||
comm_type=CommType.UDP,
|
||||
comm_name="server_listener",
|
||||
source_address=address,
|
||||
reconnect_delay=0.0,
|
||||
reconnect_delay_max=0.0,
|
||||
timeout_connect=0.0,
|
||||
),
|
||||
context,
|
||||
ignore_missing_slaves,
|
||||
broadcast_enable,
|
||||
response_manipulator,
|
||||
request_tracer,
|
||||
identity,
|
||||
framer,
|
||||
)
|
||||
|
||||
|
||||
class ModbusSerialServer(ModbusBaseServer):
|
||||
"""A modbus threaded serial socket server.
|
||||
|
||||
We inherit and overload the socket server so that we
|
||||
can control the client threads as well as have a single
|
||||
server context instance.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, context, framer=ModbusRtuFramer, identity=None, **kwargs
|
||||
): # pragma: no cover
|
||||
"""Initialize the socket server.
|
||||
|
||||
If the identity structure is not passed in, the ModbusControlBlock
|
||||
uses its own empty structure.
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param framer: The framer strategy to use, default ModbusRtuFramer
|
||||
:param identity: An optional identify structure
|
||||
:param port: The serial port to attach to
|
||||
:param stopbits: The number of stop bits to use
|
||||
:param bytesize: The bytesize of the serial messages
|
||||
:param parity: Which kind of parity to use
|
||||
:param baudrate: The baud rate to use for the serial device
|
||||
:param timeout: The timeout to use for the serial device
|
||||
:param handle_local_echo: (optional) Discard local echo from dongle.
|
||||
:param ignore_missing_slaves: True to not send errors on a request
|
||||
to a missing slave
|
||||
:param broadcast_enable: True to treat slave_id 0 as broadcast address,
|
||||
False to treat 0 as any other slave_id
|
||||
:param reconnect_delay: reconnect delay in seconds
|
||||
:param response_manipulator: Callback method for
|
||||
manipulating the response
|
||||
:param request_tracer: Callback method for tracing
|
||||
"""
|
||||
super().__init__(
|
||||
CommParams(
|
||||
comm_type=CommType.SERIAL,
|
||||
comm_name="server_listener",
|
||||
reconnect_delay=kwargs.get("reconnect_delay", 2),
|
||||
reconnect_delay_max=0.0,
|
||||
timeout_connect=kwargs.get("timeout", 3),
|
||||
source_address=(kwargs.get("port", 0), 0),
|
||||
bytesize=kwargs.get("bytesize", 8),
|
||||
parity=kwargs.get("parity", "N"),
|
||||
baudrate=kwargs.get("baudrate", 19200),
|
||||
stopbits=kwargs.get("stopbits", 1),
|
||||
),
|
||||
context,
|
||||
kwargs.get("ignore_missing_slaves", False),
|
||||
kwargs.get("broadcast_enable", False),
|
||||
kwargs.get("request_tracer", None),
|
||||
kwargs.get("response_manipulator", None),
|
||||
kwargs.get("identity", None),
|
||||
framer,
|
||||
)
|
||||
self.handle_local_echo = kwargs.get("handle_local_echo", False)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Creation Factories
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class _serverList:
|
||||
"""Maintains information about the active server.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
|
||||
active_server: Union[ModbusTcpServer, ModbusUdpServer, ModbusSerialServer] = None
|
||||
|
||||
def __init__(self, server):
|
||||
"""Register new server."""
|
||||
self.server = server
|
||||
self.loop = asyncio.get_event_loop()
|
||||
|
||||
@classmethod
|
||||
async def run(cls, server, custom_functions):
|
||||
"""Help starting/stopping server."""
|
||||
for func in custom_functions:
|
||||
server.decoder.register(func)
|
||||
cls.active_server = _serverList(server)
|
||||
with suppress(asyncio.exceptions.CancelledError):
|
||||
await server.serve_forever()
|
||||
|
||||
@classmethod
|
||||
async def async_stop(cls):
|
||||
"""Wait for server stop."""
|
||||
if not cls.active_server:
|
||||
raise RuntimeError("ServerAsyncStop called without server task active.")
|
||||
await cls.active_server.server.shutdown()
|
||||
if os.name == "nt":
|
||||
await asyncio.sleep(1)
|
||||
else:
|
||||
await asyncio.sleep(0.1)
|
||||
cls.active_server = None
|
||||
|
||||
@classmethod
|
||||
def stop(cls):
|
||||
"""Wait for server stop."""
|
||||
if not cls.active_server:
|
||||
Log.info("ServerStop called without server task active.")
|
||||
return
|
||||
if not cls.active_server.loop.is_running():
|
||||
Log.info("ServerStop called with loop stopped.")
|
||||
return
|
||||
asyncio.run_coroutine_threadsafe(cls.async_stop(), cls.active_server.loop)
|
||||
if os.name == "nt":
|
||||
time.sleep(10)
|
||||
else:
|
||||
time.sleep(0.5)
|
||||
|
||||
|
||||
async def StartAsyncTcpServer( # pylint: disable=invalid-name,dangerous-default-value
|
||||
context=None,
|
||||
identity=None,
|
||||
address=None,
|
||||
custom_functions=[],
|
||||
**kwargs,
|
||||
):
|
||||
"""Start and run a tcp modbus server.
|
||||
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param identity: An optional identify structure
|
||||
:param address: An optional (interface, port) to bind to.
|
||||
:param custom_functions: An optional list of custom function classes
|
||||
supported by server instance.
|
||||
:param kwargs: The rest
|
||||
"""
|
||||
kwargs.pop("host", None)
|
||||
server = ModbusTcpServer(
|
||||
context, kwargs.pop("framer", ModbusSocketFramer), identity, address, **kwargs
|
||||
)
|
||||
await _serverList.run(server, custom_functions)
|
||||
|
||||
|
||||
async def StartAsyncTlsServer( # pylint: disable=invalid-name,dangerous-default-value
|
||||
context=None,
|
||||
identity=None,
|
||||
address=None,
|
||||
sslctx=None,
|
||||
certfile=None,
|
||||
keyfile=None,
|
||||
password=None,
|
||||
custom_functions=[],
|
||||
**kwargs,
|
||||
):
|
||||
"""Start and run a tls modbus server.
|
||||
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param identity: An optional identify structure
|
||||
:param address: An optional (interface, port) to bind to.
|
||||
:param sslctx: The SSLContext to use for TLS (default None and auto create)
|
||||
:param certfile: The cert file path for TLS (used if sslctx is None)
|
||||
:param keyfile: The key file path for TLS (used if sslctx is None)
|
||||
:param password: The password for for decrypting the private key file
|
||||
:param custom_functions: An optional list of custom function classes
|
||||
supported by server instance.
|
||||
:param kwargs: The rest
|
||||
"""
|
||||
kwargs.pop("host", None)
|
||||
server = ModbusTlsServer(
|
||||
context,
|
||||
kwargs.pop("framer", ModbusTlsFramer),
|
||||
identity,
|
||||
address,
|
||||
sslctx,
|
||||
certfile,
|
||||
keyfile,
|
||||
password,
|
||||
**kwargs,
|
||||
)
|
||||
await _serverList.run(server, custom_functions)
|
||||
|
||||
|
||||
async def StartAsyncUdpServer( # pylint: disable=invalid-name,dangerous-default-value
|
||||
context=None,
|
||||
identity=None,
|
||||
address=None,
|
||||
custom_functions=[],
|
||||
**kwargs,
|
||||
):
|
||||
"""Start and run a udp modbus server.
|
||||
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param identity: An optional identify structure
|
||||
:param address: An optional (interface, port) to bind to.
|
||||
:param custom_functions: An optional list of custom function classes
|
||||
supported by server instance.
|
||||
:param kwargs:
|
||||
"""
|
||||
kwargs.pop("host", None)
|
||||
server = ModbusUdpServer(
|
||||
context, kwargs.pop("framer", ModbusSocketFramer), identity, address, **kwargs
|
||||
)
|
||||
await _serverList.run(server, custom_functions)
|
||||
|
||||
|
||||
async def StartAsyncSerialServer( # pylint: disable=invalid-name,dangerous-default-value
|
||||
context=None,
|
||||
identity=None,
|
||||
custom_functions=[],
|
||||
**kwargs,
|
||||
): # pragma: no cover
|
||||
"""Start and run a serial modbus server.
|
||||
|
||||
:param context: The ModbusServerContext datastore
|
||||
:param identity: An optional identify structure
|
||||
:param custom_functions: An optional list of custom function classes
|
||||
supported by server instance.
|
||||
:param kwargs: The rest
|
||||
"""
|
||||
server = ModbusSerialServer(
|
||||
context, kwargs.pop("framer", ModbusAsciiFramer), identity=identity, **kwargs
|
||||
)
|
||||
await _serverList.run(server, custom_functions)
|
||||
|
||||
|
||||
def StartSerialServer(**kwargs): # pylint: disable=invalid-name
|
||||
"""Start and run a serial modbus server."""
|
||||
return asyncio.run(StartAsyncSerialServer(**kwargs))
|
||||
|
||||
|
||||
def StartTcpServer(**kwargs): # pylint: disable=invalid-name
|
||||
"""Start and run a serial modbus server."""
|
||||
return asyncio.run(StartAsyncTcpServer(**kwargs))
|
||||
|
||||
|
||||
def StartTlsServer(**kwargs): # pylint: disable=invalid-name
|
||||
"""Start and run a serial modbus server."""
|
||||
return asyncio.run(StartAsyncTlsServer(**kwargs))
|
||||
|
||||
|
||||
def StartUdpServer(**kwargs): # pylint: disable=invalid-name
|
||||
"""Start and run a serial modbus server."""
|
||||
return asyncio.run(StartAsyncUdpServer(**kwargs))
|
||||
|
||||
|
||||
async def ServerAsyncStop(): # pylint: disable=invalid-name
|
||||
"""Terminate server."""
|
||||
await _serverList.async_stop()
|
||||
|
||||
|
||||
def ServerStop(): # pylint: disable=invalid-name
|
||||
"""Terminate server."""
|
||||
_serverList.stop()
|
||||
1
env/lib/python3.11/site-packages/pymodbus/server/reactive/__init__.py
vendored
Normal file
1
env/lib/python3.11/site-packages/pymodbus/server/reactive/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
"""Initialize."""
|
||||
BIN
env/lib/python3.11/site-packages/pymodbus/server/reactive/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/server/reactive/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/server/reactive/__pycache__/default_config.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/server/reactive/__pycache__/default_config.cpython-311.pyc
vendored
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user