venv added, updated

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

View File

@@ -0,0 +1,18 @@
"""Framer."""
__all__ = [
"ExceptionResponse",
"IllegalFunctionRequest",
"ModbusExceptions",
"ModbusPDU",
"ModbusRequest",
"ModbusResponse",
]
from pymodbus.pdu.pdu import (
ExceptionResponse,
IllegalFunctionRequest,
ModbusExceptions,
ModbusPDU,
ModbusRequest,
ModbusResponse,
)

View File

@@ -0,0 +1,262 @@
"""Bit Reading Request/Response messages."""
# pylint: disable=missing-type-doc
import struct
from pymodbus.pdu import ModbusExceptions as merror
from pymodbus.pdu import ModbusRequest, ModbusResponse
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, transaction, protocol, skip_encode):
"""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, transaction, protocol, skip_encode)
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, transaction, protocol, skip_encode):
"""Initialize a new instance.
:param values: The requested values to be returned
:param slave: Modbus slave slave ID
"""
ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode)
#: 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=1, transaction=0, protocol=0, skip_encode=False):
"""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, transaction, protocol, skip_encode)
async 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 = await context.async_getValues(
self.function_code, self.address, self.count
)
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=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new instance.
:param values: The request values to respond with
:param slave: Modbus slave slave ID
"""
ReadBitsResponseBase.__init__(self, values, slave, transaction, protocol, skip_encode)
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=1, transaction=0, protocol=0, skip_encode=False):
"""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, transaction, protocol, skip_encode)
async 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 = await context.async_getValues(
self.function_code, self.address, self.count
)
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=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new instance.
:param values: The request values to respond with
:param slave: Modbus slave slave ID
"""
ReadBitsResponseBase.__init__(self, values, slave, transaction, protocol, skip_encode)

View File

@@ -0,0 +1,282 @@
"""Bit Writing Request/Response.
TODO write mask request/response
"""
# pylint: disable=missing-type-doc
import struct
from pymodbus.constants import ModbusStatus
from pymodbus.pdu import ModbusExceptions as merror
from pymodbus.pdu import ModbusRequest, ModbusResponse
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, transaction=0, protocol=0, skip_encode=0):
"""Initialize a new instance.
:param address: The variable address to write
:param value: The value to write at address
"""
ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode)
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
async 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)
await context.async_setValues(self.function_code, self.address, [self.value])
values = await context.async_getValues(self.function_code, self.address, 1)
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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new instance.
:param address: The variable address written to
:param value: The value written at address
"""
ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode)
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, transaction=0, protocol=0, skip_encode=0):
"""Initialize a new instance.
:param address: The starting request address
:param values: The values to write
"""
ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode)
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]
async 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)
await context.async_setValues(
self.function_code, self.address, self.values
)
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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new instance.
:param address: The starting variable address written to
:param count: The number of values written
"""
ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode)
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})"

View File

@@ -0,0 +1,833 @@
"""Diagnostic Record Read/Write.
These need to be tied into a the current server context
or linked to the appropriate data
"""
# 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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a diagnostic request."""
ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode)
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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a diagnostic response."""
ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode)
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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""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, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode)
self.message = data
async 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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""Return a simple diagnostic response.
:param data: The resulting data to return to the client
"""
DiagnosticStatusResponse.__init__(self, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode)
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=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new instance of the request.
:param message: The message to send to loopback
"""
DiagnosticStatusRequest.__init__(self, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode)
if not isinstance(message, bytes):
raise ModbusException(f"message({type(message)}) must be bytes")
self.message = message
async 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", slave=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new instance of the response.
:param message: The message to loopback
"""
DiagnosticStatusResponse.__init__(self, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode)
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=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new request.
:param toggle: Set to True to toggle, False otherwise
"""
DiagnosticStatusRequest.__init__(self, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode)
if toggle:
self.message = [ModbusStatus.ON]
else:
self.message = [ModbusStatus.OFF]
async 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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new response.
:param toggle: Set to True if we toggled, False otherwise
"""
DiagnosticStatusResponse.__init__(self, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode)
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
async 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
async def execute(self, *args):
"""Execute the diagnostic request on the given device.
:returns: The initialized response message
"""
char = (self.message & 0xFF00) >> 8 # type: ignore[operator]
_MCB.Delimiter = char
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
async def execute(self, *args):
"""Execute the diagnostic request on the given device.
:returns: The initialized response message
"""
_MCB.ListenOnly = True
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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize to block a return response."""
DiagnosticStatusResponse.__init__(self, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode)
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
async 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
async 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
async 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
async 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
async 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
async 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
async 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
async 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
async 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
async 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
async 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, data=0, slave=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize."""
super().__init__(slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode)
self.message=data
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
async 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

View File

@@ -0,0 +1,428 @@
"""File Record Read/Write Messages.
Currently none of these messages are implemented
"""
from __future__ import annotations
# 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, reference_type=0x06, file_number=0x00, record_number=0x00, record_data="", record_length=None, response_length=None):
"""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 = reference_type
self.file_number = file_number
self.record_number = record_number
self.record_data = record_data
self.record_length = record_length if record_length else len(self.record_data) // 2
self.response_length = response_length if response_length else 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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new instance.
:param records: The file record requests to be read
"""
ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode)
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: list[FileRecord] = []
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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new instance.
:param records: The requested file records
"""
ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode)
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", record.record_length, 0x06)
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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new instance.
:param records: The file record requests to be read
"""
ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode)
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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new instance.
:param records: The file record requests to be read
"""
ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode)
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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new instance.
:param address: The fifo pointer address (0x0000 to 0xffff)
"""
ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode)
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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new instance.
:param values: The list of values of the fifo to return
"""
ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode)
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])

View File

@@ -0,0 +1,216 @@
"""Encapsulated Interface (MEI) Transport Messages."""
# 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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new instance.
:param read_code: The device information read code
:param object_id: The object to read from
"""
ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode)
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
async 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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new instance.
:param read_code: The device information read code
:param information: The requested information request
"""
ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode)
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 = 253 - 6
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
)
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})"

View File

@@ -0,0 +1,473 @@
"""Diagnostic record read/write.
Currently not all implemented
"""
# 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, transaction=0, protocol=0, skip_encode=0):
"""Initialize a new instance."""
ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode)
def encode(self):
"""Encode the message."""
return b""
def decode(self, data):
"""Decode data part of the message.
:param data: The incoming data
"""
async 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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new instance.
:param status: The status response to report
"""
ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode)
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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new instance."""
ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode)
def encode(self):
"""Encode the message."""
return b""
def decode(self, data):
"""Decode data part of the message.
:param data: The incoming data
"""
async 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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new instance.
:param count: The current event counter value
"""
ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode)
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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new instance."""
ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode)
def encode(self):
"""Encode the message."""
return b""
def decode(self, data):
"""Decode data part of the message.
:param data: The incoming data
"""
async 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, status=True, message_count=0, event_count=0, events=None, slave=1, transaction=0, protocol=0, skip_encode=False):
"""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, slave, transaction, protocol, skip_encode)
self.status = status
self.message_count = message_count
self.event_count = event_count
self.events = events if events else []
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=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new instance.
:param slave: Modbus slave slave ID
"""
ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode)
def encode(self):
"""Encode the message."""
return b""
def decode(self, data):
"""Decode data part of the message.
:param data: The incoming data
"""
async 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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new instance.
:param identifier: The identifier of the slave
:param status: The status response to report
"""
ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode)
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})"

View File

@@ -0,0 +1,255 @@
"""Contains base classes for modbus request/response/error packets."""
# 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, transaction, protocol, skip_encode):
"""Initialize the base data for a modbus request.
:param slave: Modbus slave slave ID
"""
self.transaction_id = transaction
self.protocol_id = protocol
self.slave_id = slave
self.skip_encode = skip_encode
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, transaction, protocol, skip_encode):
"""Proxy to the lower level initializer.
:param slave: Modbus slave slave ID
"""
super().__init__(slave, transaction, protocol, skip_encode)
self.fut = None
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, transaction, protocol, skip_encode):
"""Proxy the lower level initializer.
:param slave: Modbus slave slave ID
"""
super().__init__(slave, transaction, protocol, skip_encode)
self.bits = []
self.registers = []
self.request = None
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
NegativeAcknowledge = 0x07
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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""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__(slave, transaction, protocol, skip_encode)
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, xslave, xtransaction, xprotocol, xskip_encode):
"""Initialize a IllegalFunctionRequest.
:param function_code: The function we are erroring on
"""
super().__init__(xslave, xtransaction, xprotocol, xskip_encode)
self.function_code = function_code
def decode(self, _data):
"""Decode so this failure will run correctly."""
def encode(self):
"""Decode so this failure will run correctly."""
async def execute(self, _context):
"""Build an illegal function request error response.
:returns: The error response packet
"""
return ExceptionResponse(self.function_code, self.ErrorCode)

View File

@@ -0,0 +1,367 @@
"""Register Reading Request/Response."""
# pylint: disable=missing-type-doc
import struct
from pymodbus.exceptions import ModbusIOException
from pymodbus.pdu import ModbusExceptions as merror
from pymodbus.pdu import ModbusRequest, ModbusResponse
class ReadRegistersRequestBase(ModbusRequest):
"""Base class for reading a modbus register."""
_rtu_frame_size = 8
def __init__(self, address, count, slave=1, transaction=0, protocol=0, skip_encode=False):
"""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, transaction, protocol, skip_encode)
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=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new instance.
:param values: The values to write to
:param slave: Modbus slave slave ID
"""
super().__init__(slave, transaction, protocol, skip_encode)
#: 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])
if byte_count < 2 or byte_count > 252 or byte_count % 2 == 1 or byte_count != len(data) - 1:
raise ModbusIOException(f"Invalid response {data} has byte count of {byte_count}")
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=1, transaction=0, protocol=0, skip_encode=0):
"""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, transaction, protocol, skip_encode)
async 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`
"""
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 = await context.async_getValues(
self.function_code, self.address, self.count
)
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, slave=None, transaction=0, protocol=0, skip_encode=0):
"""Initialize a new response instance.
:param values: The resulting register values
"""
super().__init__(values, slave, transaction, protocol, skip_encode)
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=1, transaction=0, protocol=0, skip_encode=0):
"""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, transaction, protocol, skip_encode)
async 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`
"""
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 = await context.async_getValues(
self.function_code, self.address, self.count
)
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, slave=None, transaction=0, protocol=0, skip_encode=0):
"""Initialize a new response instance.
:param values: The resulting register values
"""
super().__init__(values, slave, transaction, protocol, skip_encode)
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, read_address=0x00, read_count=0, write_address=0x00, write_registers=None, slave=1, transaction=0, protocol=0, skip_encode=False):
"""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__(slave, transaction, protocol, skip_encode)
self.read_address = read_address
self.read_count = read_count
self.write_address = write_address
self.write_registers = write_registers
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)
async 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`
"""
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)
await context.async_setValues(
self.function_code, self.write_address, self.write_registers
)
registers = await context.async_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(ReadHoldingRegistersResponse):
"""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

View File

@@ -0,0 +1,369 @@
"""Register Writing Request/Response Messages."""
# pylint: disable=missing-type-doc
import struct
from pymodbus.pdu import ModbusExceptions as merror
from pymodbus.pdu import ModbusRequest, ModbusResponse
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, transaction=0, protocol=0, skip_encode=0):
"""Initialize a new instance.
:param address: The address to start writing add
:param value: The values to write
"""
super().__init__(slave, transaction, protocol, skip_encode)
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)
async 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)
await context.async_setValues(
self.function_code, self.address, [self.value]
)
values = await context.async_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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new instance.
:param address: The address to start writing add
:param value: The values to write
"""
super().__init__(slave, transaction, protocol, skip_encode)
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, transaction=0, protocol=0, skip_encode=0):
"""Initialize a new instance.
:param address: The address to start writing to
:param values: The values to write
"""
super().__init__(slave, transaction, protocol, skip_encode)
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])
async 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)
await context.async_setValues(
self.function_code, self.address, self.values
)
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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""Initialize a new instance.
:param address: The address to start writing to
:param count: The number of registers to write to
"""
super().__init__(slave, transaction, protocol, skip_encode)
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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""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__(slave, transaction, protocol, skip_encode)
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)
async 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 = (await context.async_getValues(self.function_code, self.address, 1))[0]
values = (values & self.and_mask) | (self.or_mask & ~self.and_mask)
await context.async_setValues(
self.function_code, self.address, [values]
)
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, slave=1, transaction=0, protocol=0, skip_encode=False):
"""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__(slave, transaction, protocol, skip_encode)
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)