venv added, updated
This commit is contained in:
18
myenv/lib/python3.12/site-packages/pymodbus/pdu/__init__.py
Normal file
18
myenv/lib/python3.12/site-packages/pymodbus/pdu/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Framer."""
|
||||
__all__ = [
|
||||
"ExceptionResponse",
|
||||
"IllegalFunctionRequest",
|
||||
"ModbusExceptions",
|
||||
"ModbusPDU",
|
||||
"ModbusRequest",
|
||||
"ModbusResponse",
|
||||
]
|
||||
|
||||
from pymodbus.pdu.pdu import (
|
||||
ExceptionResponse,
|
||||
IllegalFunctionRequest,
|
||||
ModbusExceptions,
|
||||
ModbusPDU,
|
||||
ModbusRequest,
|
||||
ModbusResponse,
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
@@ -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})"
|
||||
833
myenv/lib/python3.12/site-packages/pymodbus/pdu/diag_message.py
Normal file
833
myenv/lib/python3.12/site-packages/pymodbus/pdu/diag_message.py
Normal 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
|
||||
428
myenv/lib/python3.12/site-packages/pymodbus/pdu/file_message.py
Normal file
428
myenv/lib/python3.12/site-packages/pymodbus/pdu/file_message.py
Normal 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])
|
||||
216
myenv/lib/python3.12/site-packages/pymodbus/pdu/mei_message.py
Normal file
216
myenv/lib/python3.12/site-packages/pymodbus/pdu/mei_message.py
Normal 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})"
|
||||
473
myenv/lib/python3.12/site-packages/pymodbus/pdu/other_message.py
Normal file
473
myenv/lib/python3.12/site-packages/pymodbus/pdu/other_message.py
Normal 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})"
|
||||
255
myenv/lib/python3.12/site-packages/pymodbus/pdu/pdu.py
Normal file
255
myenv/lib/python3.12/site-packages/pymodbus/pdu/pdu.py
Normal 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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user