504 lines
18 KiB
Python
504 lines
18 KiB
Python
"""Modbus Client Common."""
|
|
from __future__ import annotations
|
|
|
|
import struct
|
|
from enum import Enum
|
|
from typing import Generic, TypeVar
|
|
|
|
import pymodbus.pdu.bit_read_message as pdu_bit_read
|
|
import pymodbus.pdu.bit_write_message as pdu_bit_write
|
|
import pymodbus.pdu.diag_message as pdu_diag
|
|
import pymodbus.pdu.file_message as pdu_file_msg
|
|
import pymodbus.pdu.mei_message as pdu_mei
|
|
import pymodbus.pdu.other_message as pdu_other_msg
|
|
import pymodbus.pdu.register_read_message as pdu_reg_read
|
|
import pymodbus.pdu.register_write_message as pdu_req_write
|
|
from pymodbus.exceptions import ModbusException
|
|
from pymodbus.pdu import ModbusRequest
|
|
|
|
|
|
T = TypeVar("T", covariant=False)
|
|
|
|
|
|
class ModbusClientMixin(Generic[T]): # pylint: disable=too-many-public-methods
|
|
"""**ModbusClientMixin**.
|
|
|
|
This is an interface class to facilitate the sending requests/receiving responses like read_coils.
|
|
execute() allows to make a call with non-standard or user defined function codes (remember to add a PDU
|
|
in the transport class to interpret the request/response).
|
|
|
|
Simple modbus message call::
|
|
|
|
response = client.read_coils(1, 10)
|
|
# or
|
|
response = await client.read_coils(1, 10)
|
|
|
|
Advanced modbus message call::
|
|
|
|
request = ReadCoilsRequest(1,10)
|
|
response = client.execute(request)
|
|
# or
|
|
request = ReadCoilsRequest(1,10)
|
|
response = await client.execute(request)
|
|
|
|
.. tip::
|
|
All methods can be used directly (synchronous) or
|
|
with await <method> (asynchronous) depending on the client used.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize."""
|
|
|
|
def execute(self, _request: ModbusRequest) -> T:
|
|
"""Execute request (code ???).
|
|
|
|
:raises ModbusException:
|
|
|
|
Call with custom function codes.
|
|
|
|
.. tip::
|
|
Response is not interpreted.
|
|
"""
|
|
raise NotImplementedError("execute of ModbusClientMixin needs to be overridden")
|
|
|
|
def read_coils(self, address: int, count: int = 1, slave: int = 1) -> T:
|
|
"""Read coils (code 0x01).
|
|
|
|
:param address: Start address to read from
|
|
:param count: (optional) Number of coils to read
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(pdu_bit_read.ReadCoilsRequest(address, count, slave=slave))
|
|
|
|
def read_discrete_inputs(self, address: int, count: int = 1, slave: int = 1) -> T:
|
|
"""Read discrete inputs (code 0x02).
|
|
|
|
:param address: Start address to read from
|
|
:param count: (optional) Number of coils to read
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(pdu_bit_read.ReadDiscreteInputsRequest(address, count, slave=slave))
|
|
|
|
def read_holding_registers(self, address: int, count: int = 1, slave: int = 1) -> T:
|
|
"""Read holding registers (code 0x03).
|
|
|
|
:param address: Start address to read from
|
|
:param count: (optional) Number of coils to read
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(pdu_reg_read.ReadHoldingRegistersRequest(address, count, slave=slave))
|
|
|
|
def read_input_registers(self, address: int, count: int = 1, slave: int = 1) -> T:
|
|
"""Read input registers (code 0x04).
|
|
|
|
:param address: Start address to read from
|
|
:param count: (optional) Number of coils to read
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(pdu_reg_read.ReadInputRegistersRequest(address, count, slave=slave))
|
|
|
|
def write_coil(self, address: int, value: bool, slave: int = 1) -> T:
|
|
"""Write single coil (code 0x05).
|
|
|
|
:param address: Address to write to
|
|
:param value: Boolean to write
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(pdu_bit_write.WriteSingleCoilRequest(address, value, slave=slave))
|
|
|
|
def write_register(self, address: int, value: int, slave: int = 1) -> T:
|
|
"""Write register (code 0x06).
|
|
|
|
:param address: Address to write to
|
|
:param value: Value to write
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(pdu_req_write.WriteSingleRegisterRequest(address, value, slave=slave))
|
|
|
|
def read_exception_status(self, slave: int = 1) -> T:
|
|
"""Read Exception Status (code 0x07).
|
|
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(pdu_other_msg.ReadExceptionStatusRequest(slave=slave))
|
|
|
|
|
|
def diag_query_data(
|
|
self, msg: bytes, slave: int = 1) -> T:
|
|
"""Diagnose query data (code 0x08 sub 0x00).
|
|
|
|
:param msg: Message to be returned
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(pdu_diag.ReturnQueryDataRequest(msg, slave=slave))
|
|
|
|
def diag_restart_communication(
|
|
self, toggle: bool, slave: int = 1) -> T:
|
|
"""Diagnose restart communication (code 0x08 sub 0x01).
|
|
|
|
:param toggle: True if toggled.
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(
|
|
pdu_diag.RestartCommunicationsOptionRequest(toggle, slave=slave)
|
|
)
|
|
|
|
def diag_read_diagnostic_register(self, slave: int = 1) -> T:
|
|
"""Diagnose read diagnostic register (code 0x08 sub 0x02).
|
|
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(
|
|
pdu_diag.ReturnDiagnosticRegisterRequest(slave=slave)
|
|
)
|
|
|
|
def diag_change_ascii_input_delimeter(self, slave: int = 1) -> T:
|
|
"""Diagnose change ASCII input delimiter (code 0x08 sub 0x03).
|
|
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(
|
|
pdu_diag.ChangeAsciiInputDelimiterRequest(slave=slave)
|
|
)
|
|
|
|
def diag_force_listen_only(self, slave: int = 1) -> T:
|
|
"""Diagnose force listen only (code 0x08 sub 0x04).
|
|
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(pdu_diag.ForceListenOnlyModeRequest(slave=slave))
|
|
|
|
def diag_clear_counters(self, slave: int = 1) -> T:
|
|
"""Diagnose clear counters (code 0x08 sub 0x0A).
|
|
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(pdu_diag.ClearCountersRequest(slave=slave))
|
|
|
|
def diag_read_bus_message_count(self, slave: int = 1) -> T:
|
|
"""Diagnose read bus message count (code 0x08 sub 0x0B).
|
|
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(
|
|
pdu_diag.ReturnBusMessageCountRequest(slave=slave)
|
|
)
|
|
|
|
def diag_read_bus_comm_error_count(self, slave: int = 1) -> T:
|
|
"""Diagnose read Bus Communication Error Count (code 0x08 sub 0x0C).
|
|
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(
|
|
pdu_diag.ReturnBusCommunicationErrorCountRequest(slave=slave)
|
|
)
|
|
|
|
def diag_read_bus_exception_error_count(self, slave: int = 1) -> T:
|
|
"""Diagnose read Bus Exception Error Count (code 0x08 sub 0x0D).
|
|
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(
|
|
pdu_diag.ReturnBusExceptionErrorCountRequest(slave=slave)
|
|
)
|
|
|
|
def diag_read_slave_message_count(self, slave: int = 1) -> T:
|
|
"""Diagnose read Slave Message Count (code 0x08 sub 0x0E).
|
|
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(
|
|
pdu_diag.ReturnSlaveMessageCountRequest(slave=slave)
|
|
)
|
|
|
|
def diag_read_slave_no_response_count(self, slave: int = 1) -> T:
|
|
"""Diagnose read Slave No Response Count (code 0x08 sub 0x0F).
|
|
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(
|
|
pdu_diag.ReturnSlaveNoResponseCountRequest(slave=slave)
|
|
)
|
|
|
|
def diag_read_slave_nak_count(self, slave: int = 1) -> T:
|
|
"""Diagnose read Slave NAK Count (code 0x08 sub 0x10).
|
|
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(pdu_diag.ReturnSlaveNAKCountRequest(slave=slave))
|
|
|
|
def diag_read_slave_busy_count(self, slave: int = 1) -> T:
|
|
"""Diagnose read Slave Busy Count (code 0x08 sub 0x11).
|
|
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(pdu_diag.ReturnSlaveBusyCountRequest(slave=slave))
|
|
|
|
def diag_read_bus_char_overrun_count(self, slave: int = 1) -> T:
|
|
"""Diagnose read Bus Character Overrun Count (code 0x08 sub 0x12).
|
|
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(
|
|
pdu_diag.ReturnSlaveBusCharacterOverrunCountRequest(slave=slave)
|
|
)
|
|
|
|
def diag_read_iop_overrun_count(self, slave: int = 1) -> T:
|
|
"""Diagnose read Iop overrun count (code 0x08 sub 0x13).
|
|
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(
|
|
pdu_diag.ReturnIopOverrunCountRequest(slave=slave)
|
|
)
|
|
|
|
def diag_clear_overrun_counter(self, slave: int = 1) -> T:
|
|
"""Diagnose Clear Overrun Counter and Flag (code 0x08 sub 0x14).
|
|
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(pdu_diag.ClearOverrunCountRequest(slave=slave))
|
|
|
|
def diag_getclear_modbus_response(self, slave: int = 1) -> T:
|
|
"""Diagnose Get/Clear modbus plus (code 0x08 sub 0x15).
|
|
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(pdu_diag.GetClearModbusPlusRequest(slave=slave))
|
|
|
|
def diag_get_comm_event_counter(self, slave: int = 1) -> T:
|
|
"""Diagnose get event counter (code 0x0B).
|
|
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(pdu_other_msg.GetCommEventCounterRequest(slave=slave))
|
|
|
|
def diag_get_comm_event_log(self, slave: int = 1) -> T:
|
|
"""Diagnose get event counter (code 0x0C).
|
|
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(pdu_other_msg.GetCommEventLogRequest(slave=slave))
|
|
|
|
def write_coils(
|
|
self,
|
|
address: int,
|
|
values: list[bool] | bool,
|
|
slave: int = 1,
|
|
) -> T:
|
|
"""Write coils (code 0x0F).
|
|
|
|
:param address: Start address to write to
|
|
:param values: List of booleans to write, or a single boolean to write
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(
|
|
pdu_bit_write.WriteMultipleCoilsRequest(address, values, slave)
|
|
)
|
|
|
|
def write_registers(
|
|
self, address: int, values: list[int], slave: int = 1, skip_encode: bool = False) -> T:
|
|
"""Write registers (code 0x10).
|
|
|
|
:param address: Start address to write to
|
|
:param values: List of values to write
|
|
:param slave: (optional) Modbus slave ID
|
|
:param skip_encode: (optional) do not encode values
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(
|
|
pdu_req_write.WriteMultipleRegistersRequest(address, values, slave=slave, skip_encode=skip_encode)
|
|
)
|
|
|
|
def report_slave_id(self, slave: int = 1) -> T:
|
|
"""Report slave ID (code 0x11).
|
|
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(pdu_other_msg.ReportSlaveIdRequest(slave=slave))
|
|
|
|
def read_file_record(self, records: list[tuple], slave: int = 1) -> T:
|
|
"""Read file record (code 0x14).
|
|
|
|
:param records: List of (Reference type, File number, Record Number, Record Length)
|
|
:param slave: device id
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(pdu_file_msg.ReadFileRecordRequest(records, slave=slave))
|
|
|
|
def write_file_record(self, records: list[tuple], slave: int = 1) -> T:
|
|
"""Write file record (code 0x15).
|
|
|
|
:param records: List of (Reference type, File number, Record Number, Record Length)
|
|
:param slave: (optional) Device id
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(pdu_file_msg.WriteFileRecordRequest(records, slave=slave))
|
|
|
|
def mask_write_register(
|
|
self,
|
|
address: int = 0x0000,
|
|
and_mask: int = 0xFFFF,
|
|
or_mask: int = 0x0000,
|
|
slave: int = 1,
|
|
) -> T:
|
|
"""Mask write register (code 0x16).
|
|
|
|
:param address: The mask pointer address (0x0000 to 0xffff)
|
|
:param and_mask: The and bitmask to apply to the register address
|
|
:param or_mask: The or bitmask to apply to the register address
|
|
:param slave: (optional) device id
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(
|
|
pdu_req_write.MaskWriteRegisterRequest(address, and_mask, or_mask, slave=slave)
|
|
)
|
|
|
|
def readwrite_registers(
|
|
self,
|
|
read_address: int = 0,
|
|
read_count: int = 0,
|
|
write_address: int = 0,
|
|
address: int | None = None,
|
|
values: list[int] | int = 0,
|
|
slave: int = 1,
|
|
) -> T:
|
|
"""Read/Write registers (code 0x17).
|
|
|
|
:param read_address: The address to start reading from
|
|
:param read_count: The number of registers to read from address
|
|
:param write_address: The address to start writing to
|
|
:param address: (optional) use as read/write address
|
|
:param values: List of values to write, or a single value to write
|
|
:param slave: (optional) Modbus slave ID
|
|
:raises ModbusException:
|
|
"""
|
|
if address:
|
|
read_address = address
|
|
write_address = address
|
|
return self.execute(
|
|
pdu_reg_read.ReadWriteMultipleRegistersRequest(
|
|
read_address=read_address,
|
|
read_count=read_count,
|
|
write_address=write_address,
|
|
write_registers=values,
|
|
slave=slave,
|
|
)
|
|
)
|
|
|
|
def read_fifo_queue(self, address: int = 0x0000, slave: int = 1) -> T:
|
|
"""Read FIFO queue (code 0x18).
|
|
|
|
:param address: The address to start reading from
|
|
:param slave: (optional) device id
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(pdu_file_msg.ReadFifoQueueRequest(address, slave=slave))
|
|
|
|
# code 0x2B sub 0x0D: CANopen General Reference Request and Response, NOT IMPLEMENTED
|
|
|
|
def read_device_information(
|
|
self, read_code: int | None = None, object_id: int = 0x00, slave: int = 1) -> T:
|
|
"""Read FIFO queue (code 0x2B sub 0x0E).
|
|
|
|
:param read_code: The device information read code
|
|
:param object_id: The object to read from
|
|
:param slave: (optional) Device id
|
|
:raises ModbusException:
|
|
"""
|
|
return self.execute(
|
|
pdu_mei.ReadDeviceInformationRequest(read_code, object_id, slave=slave)
|
|
)
|
|
|
|
# ------------------
|
|
# Converter methods
|
|
# ------------------
|
|
|
|
class DATATYPE(Enum):
|
|
"""Datatype enum (name and number of bytes), used for convert_* calls."""
|
|
|
|
INT16 = ("h", 1)
|
|
UINT16 = ("H", 1)
|
|
INT32 = ("i", 2)
|
|
UINT32 = ("I", 2)
|
|
INT64 = ("q", 4)
|
|
UINT64 = ("Q", 4)
|
|
FLOAT32 = ("f", 2)
|
|
FLOAT64 = ("d", 4)
|
|
STRING = ("s", 0)
|
|
|
|
@classmethod
|
|
def convert_from_registers(
|
|
cls, registers: list[int], data_type: DATATYPE
|
|
) -> int | float | str:
|
|
"""Convert registers to int/float/str.
|
|
|
|
:param registers: list of registers received from e.g. read_holding_registers()
|
|
:param data_type: data type to convert to
|
|
:returns: int, float or str depending on "data_type"
|
|
:raises ModbusException: when size of registers is not 1, 2 or 4
|
|
"""
|
|
byte_list = bytearray()
|
|
for x in registers:
|
|
byte_list.extend(int.to_bytes(x, 2, "big"))
|
|
if data_type == cls.DATATYPE.STRING:
|
|
if byte_list[-1:] == b"\00":
|
|
byte_list = byte_list[:-1]
|
|
return byte_list.decode("utf-8")
|
|
if len(registers) != data_type.value[1]:
|
|
raise ModbusException(
|
|
f"Illegal size ({len(registers)}) of register array, cannot convert!"
|
|
)
|
|
return struct.unpack(f">{data_type.value[0]}", byte_list)[0]
|
|
|
|
@classmethod
|
|
def convert_to_registers(
|
|
cls, value: int | float | str, data_type: DATATYPE
|
|
) -> list[int]:
|
|
"""Convert int/float/str to registers (16/32/64 bit).
|
|
|
|
:param value: value to be converted
|
|
:param data_type: data type to be encoded as registers
|
|
:returns: List of registers, can be used directly in e.g. write_registers()
|
|
:raises TypeError: when there is a mismatch between data_type and value
|
|
"""
|
|
if data_type == cls.DATATYPE.STRING:
|
|
if not isinstance(value, str):
|
|
raise TypeError(f"Value should be string but is {type(value)}.")
|
|
byte_list = value.encode()
|
|
if len(byte_list) % 2:
|
|
byte_list += b"\x00"
|
|
else:
|
|
byte_list = struct.pack(f">{data_type.value[0]}", value)
|
|
regs = [
|
|
int.from_bytes(byte_list[x : x + 2], "big")
|
|
for x in range(0, len(byte_list), 2)
|
|
]
|
|
return regs
|