2024-07-19 18:35:10 +00:00
|
|
|
import struct
|
|
|
|
import random
|
|
|
|
|
|
|
|
from Communication.SocketConnection import SocketConnection
|
|
|
|
|
|
|
|
# All MODBUS/TCP ADU are sent via TCP to registered port 502.
|
|
|
|
# Remark : the different fields are encoded in Big-endian
|
|
|
|
|
|
|
|
# A Modbus frame is composed of an Application Data Unit (ADU), which encloses a Protocol Data Unit (PDU).
|
|
|
|
# A dedicated header is used on TCP/IP to identify the MODBUS Application Data Unit.
|
|
|
|
# It is called the MBAP header (MODBUS Application Protocol header)
|
|
|
|
#
|
|
|
|
# ADU = MBAP Header + PDU,
|
|
|
|
# PDU = Function code + Data.
|
|
|
|
|
|
|
|
# Below the components of the Modbus TCP/IP are listed together with their size in bytes:
|
|
|
|
# +---------------------------------+
|
|
|
|
# | Application Data Unit (ADU) |
|
|
|
|
# +---------------+-----------------+
|
|
|
|
# | **Component** | **Size** (bytes)|
|
|
|
|
# +---------------+-----------------+
|
|
|
|
# | MBAP Header | 7 |
|
|
|
|
# +---------------+-----------------+
|
|
|
|
# | PDU | N |
|
|
|
|
# +---------------+-----------------+
|
|
|
|
|
|
|
|
# The MBAP header is 7 bytes long and contains the following fields:
|
|
|
|
# +------------------------+--------------------+--------------------------------------+
|
|
|
|
# | Modbus Application Protocol header (MBAP) |
|
|
|
|
# +------------------------+--------------------+--------------------------------------+
|
|
|
|
# | **Field** | **Length** (bytes) | **Description** |
|
|
|
|
# +------------------------+--------------------+--------------------------------------+
|
|
|
|
# | Transaction identifier | 2 | Identification of a |
|
|
|
|
# | | | Modbus request/response transaction. |
|
|
|
|
# +------------------------+--------------------+--------------------------------------+
|
|
|
|
# | Protocol identifier | 2 | Protocol ID, 0 = Modbus protocol |
|
|
|
|
# +------------------------+--------------------+--------------------------------------+
|
|
|
|
# | Length | 2 | Number of following bytes |
|
|
|
|
# +------------------------+--------------------+--------------------------------------+
|
|
|
|
# | Unit identifier | 1 | Identification of a remote slave |
|
|
|
|
# | | | connected on a serial line or bus |
|
|
|
|
# +------------------------+--------------------+--------------------------------------+
|
|
|
|
|
|
|
|
# The Protocol Data Unit has a variable length and consists of the following fields:
|
|
|
|
# +------------------------+--------------------+--------------------------------------+
|
|
|
|
# | Protocol Data Unit (PDU) |
|
|
|
|
# +------------------------+--------------------+--------------------------------------+
|
|
|
|
# | **Field** | **Length** (bytes) | **Description** |
|
|
|
|
# +------------------------+--------------------+--------------------------------------+
|
|
|
|
# | Function code | 1 | Function codes as in other variants |
|
|
|
|
# +------------------------+--------------------+--------------------------------------+
|
|
|
|
# | Data | n | Data as response or commands |
|
|
|
|
# +------------------------+--------------------+--------------------------------------+
|
|
|
|
|
|
|
|
# Response:
|
|
|
|
# For a normal response, slave repeats the function code. Should a slave want to report an error,
|
|
|
|
# it will reply with the requested function code plus 128 (hex 0x80) (3 becomes 131 = hex 0x83),
|
|
|
|
# and will only include one byte of data, known as the exception code.
|
|
|
|
|
|
|
|
# A normal response frame would look like this:
|
|
|
|
# +----------------------------------+
|
|
|
|
# | Response (ADU) |
|
|
|
|
# +----------------+-----------------+
|
|
|
|
# | **Component** | **Size** (bytes)|
|
|
|
|
# +----------------+-----------------+
|
|
|
|
# | MBAP Header | 7 |
|
|
|
|
# +----------------+-----------------+
|
|
|
|
# | Function code | 1 |
|
|
|
|
# +----------------+-----------------+
|
|
|
|
# | Data | n |
|
|
|
|
# +----------------+-----------------+
|
|
|
|
|
|
|
|
# An error response frame is 9 bytes long with the following fields
|
|
|
|
# +----------------------------------+
|
|
|
|
# | Response (ADU) (Error) |
|
|
|
|
# +----------------+-----------------+
|
|
|
|
# | **Component** | **Size** (bytes)|
|
|
|
|
# +----------------+-----------------+
|
|
|
|
# | MBAP Header | 7 |
|
|
|
|
# +----------------+-----------------+
|
|
|
|
# | Function code | 1 |
|
|
|
|
# +----------------+-----------------+
|
|
|
|
# | Exception code | 1 |
|
|
|
|
# +----------------+-----------------+
|
|
|
|
|
|
|
|
# For more information on modbus:
|
|
|
|
# http://www.modbus.org
|
|
|
|
# http://www.modbus.org/docs/Modbus_Application_Protocol_V1_1b.pdf
|
|
|
|
# http://www.modbus.org/docs/Modbus_Messaging_Implementation_Guide_V1_0b.pdf
|
|
|
|
|
|
|
|
|
|
|
|
class ModbusTCP:
|
|
|
|
"""
|
|
|
|
A Modbus communication class designed for use with modbusTCP
|
|
|
|
"""
|
|
|
|
__version__ = '0.1'
|
|
|
|
|
|
|
|
# Modbus function code
|
|
|
|
READ_COILS = 0x01
|
|
|
|
READ_HOLDING_REGISTERS = 0x03
|
|
|
|
|
|
|
|
# Todo: Implement the following modbus functionality:
|
|
|
|
# READ_DISCRETE_INPUTS = 0x02
|
|
|
|
# READ_INPUT_REGISTERS = 0x04
|
|
|
|
# WRITE_SINGLE_COIL = 0x05
|
|
|
|
# WRITE_SINGLE_REGISTER = 0x06
|
|
|
|
# WRITE_MULTIPLE_COILS = 0x0F
|
|
|
|
# WRITE_MULTIPLE_REGISTERS = 0x10
|
|
|
|
|
|
|
|
# Todo: Implement error checking with the following exception codes
|
|
|
|
# Modbus exception code
|
|
|
|
# ILLEGAL_FUNCTION_CODE = 0x01
|
|
|
|
# ILLEGAL_DATA_ACCESS = 0x02 # if the request address is illegal
|
|
|
|
# ILLEGAL_DATA_VALUE = 0x03 # if the request data is invalid
|
|
|
|
|
|
|
|
def __init__(self, host, port=502):
|
|
|
|
"""
|
|
|
|
:param host: IP address to connect with
|
|
|
|
:param port: Pot (standard 502) to connect with
|
|
|
|
"""
|
|
|
|
self.__transaction_id = 0 # For synchronization between messages of server and client
|
|
|
|
self.__protocol_id = 0 # 0 for Modbus/TCP
|
|
|
|
self.__unit_id = 0 # Slave address (255 if not used)
|
|
|
|
|
|
|
|
self.pretty_print_response = False # Check to print out response message in console
|
|
|
|
|
|
|
|
self.connection = SocketConnection(host, port)
|
|
|
|
|
|
|
|
def open(self):
|
|
|
|
"""
|
|
|
|
Open the socket for communication
|
|
|
|
"""
|
|
|
|
self.connection.connect()
|
|
|
|
|
|
|
|
def close(self):
|
|
|
|
"""
|
|
|
|
Close the socket
|
|
|
|
"""
|
|
|
|
self.connection.disconnect()
|
|
|
|
|
|
|
|
def read_coils(self, bit_address, quantity=1):
|
|
|
|
""" Main function 1 of Modbus/TCP - 0x01
|
|
|
|
|
|
|
|
:param bit_address:
|
|
|
|
:param quantity:
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
data_bytes = struct.pack(">HH", bit_address, quantity)
|
|
|
|
message = self._create_message(self.READ_COILS, data_bytes)
|
|
|
|
return self._send(message)
|
|
|
|
|
|
|
|
def read_holding_registers(self, reg_address, quantity=1):
|
|
|
|
"""Main function 3 of Modbus/TCP - 0x03.
|
|
|
|
|
|
|
|
Reads the values stored in the registers at the specified addresses.
|
|
|
|
:param reg_address: Address of first register to read (16-bit) specified in bytes.
|
|
|
|
:param quantity: Number of registers to read (16-bit) specified in bytes
|
|
|
|
:return: The values stored in the addresses specified in Bytes
|
|
|
|
"""
|
|
|
|
data_bytes = struct.pack(">HH", reg_address, quantity)
|
|
|
|
message = self._create_message(self.READ_HOLDING_REGISTERS, data_bytes)
|
|
|
|
return self._send(message)
|
|
|
|
|
|
|
|
def _create_message(self, function_code, data_bytes):
|
|
|
|
"""
|
|
|
|
Create packet in bytes format for sending.
|
|
|
|
:param function_code: bytes
|
|
|
|
:param data_bytes: bytes
|
|
|
|
:return: Bytes modbus packet
|
|
|
|
"""
|
|
|
|
body = struct.pack('>B', function_code) + data_bytes # create PDU
|
|
|
|
self.__transaction_id = random.randint(0, 65535)
|
|
|
|
message_length = 1 + len(body)
|
|
|
|
header = struct.pack(">HHHB", self.__transaction_id, self.__protocol_id, message_length, self.__unit_id)
|
|
|
|
return header + body
|
|
|
|
|
|
|
|
def _send(self, adu):
|
|
|
|
""" Send message over the socket
|
|
|
|
|
|
|
|
:param adu: The data to send over the socket
|
|
|
|
:return: Bytes response from the other end of the socket
|
|
|
|
"""
|
|
|
|
self.open()
|
|
|
|
self.connection.send(adu)
|
|
|
|
response = self.connection.receive()
|
|
|
|
self.close()
|
|
|
|
|
|
|
|
if self.pretty_print_response:
|
|
|
|
self.pretty_print(response)
|
|
|
|
|
|
|
|
if self._error_check(response):
|
|
|
|
return None
|
|
|
|
return response
|
|
|
|
|
|
|
|
def _error_check(self, response):
|
|
|
|
""" Check if the frame is void of errors
|
|
|
|
|
|
|
|
Raises an exception termination the program
|
|
|
|
:param response: The ADU to check
|
|
|
|
:return: None
|
|
|
|
"""
|
|
|
|
mbap = response[:7]
|
|
|
|
function_code = response[7:8]
|
|
|
|
mbap = struct.unpack(">HHHB", mbap)
|
|
|
|
|
|
|
|
if mbap[0] != self.__transaction_id:
|
|
|
|
print("Modbus: Transaction ID mismatch"
|
2024-07-19 19:48:42 +00:00
|
|
|
"\t - Send: {} \t - Response: {}".format(self.__transaction_id, mbap[0]))
|
2024-07-19 18:35:10 +00:00
|
|
|
return True
|
|
|
|
elif mbap[1] != self.__protocol_id:
|
|
|
|
print("Modbus: Protocol ID mismatch"
|
2024-07-19 19:48:42 +00:00
|
|
|
"\t - Send: {} \t - Response: {}".format(self.__protocol_id, mbap[1]))
|
2024-07-19 18:35:10 +00:00
|
|
|
return True
|
|
|
|
elif mbap[3] != self.__unit_id:
|
|
|
|
print("Modbus: Unit ID mismatch"
|
2024-07-19 19:48:42 +00:00
|
|
|
"\t - Send: {} \t - Response: {}".format(self.__unit_id, mbap[3]))
|
2024-07-19 18:35:10 +00:00
|
|
|
return True
|
|
|
|
elif mbap[2] != len(response[6:]):
|
|
|
|
print("Modbus: Length mismatch"
|
2024-07-19 19:48:42 +00:00
|
|
|
"\t - Length: {} \t - Remaining: {}".format(mbap[2], len(response[6:])))
|
2024-07-19 18:35:10 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
function_code = struct.unpack(">B", function_code)
|
|
|
|
if function_code[0] > 127:
|
|
|
|
error_code = struct.unpack(">B", response[8:9])
|
|
|
|
print("Modbus: Function error: {}".format(error_code))
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
def set_pretty_print(self, value):
|
|
|
|
"""
|
|
|
|
Enable or disable printing of response message in console
|
|
|
|
:param value: Boolean
|
|
|
|
"""
|
|
|
|
self.pretty_print_response = value
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def pretty_print(response):
|
|
|
|
""" Print Response in the console
|
|
|
|
|
|
|
|
Unpacks the MBAP and function code in readable format
|
|
|
|
Data bytes
|
|
|
|
:param response:
|
|
|
|
"""
|
|
|
|
mbap = response[:7]
|
|
|
|
function_code = response[7:8]
|
|
|
|
mbap = struct.unpack(">HHHB", mbap)
|
|
|
|
function_code = struct.unpack(">B", function_code)
|
|
|
|
|
|
|
|
print("+--------------------------------------+")
|
|
|
|
print("| ****Modbus TCP Response (ADU)**** |")
|
|
|
|
print("+--------------------------------------+")
|
|
|
|
print("| **Header information (MBAP)** |")
|
|
|
|
print("+--------------------------------------+")
|
|
|
|
print("| Transaction id: " + str(mbap[0]))
|
|
|
|
print("| Protocol id: " + str(mbap[1]))
|
|
|
|
print("| Length: " + str(mbap[2]))
|
|
|
|
print("| Unit id: " + str(mbap[3]))
|
|
|
|
print("+--------------------------------------+")
|
|
|
|
print("| **Data information (PDU)** |")
|
|
|
|
print("+--------------------------------------+")
|
|
|
|
print("| Function code: " + str(function_code[0]))
|
|
|
|
print("| Data: " + str(response[8:]))
|
|
|
|
print("+--------------------------------------+")
|
|
|
|
print("\n")
|