|
@@ -0,0 +1,987 @@
|
|
|
+#!/usr/bin/env python
|
|
|
+# -*- encoding: utf-8 -*-
|
|
|
+
|
|
|
+from __future__ import with_statement
|
|
|
+
|
|
|
+import struct
|
|
|
+import threading
|
|
|
+import re
|
|
|
+
|
|
|
+from utils.modbus import LOGGER
|
|
|
+from utils.modbus import defines
|
|
|
+from utils.modbus.exceptions import (
|
|
|
+ ModbusError, ModbusFunctionNotSupportedError, DuplicatedKeyError, MissingKeyError, InvalidModbusBlockError,
|
|
|
+ InvalidArgumentError, OverlapModbusBlockError, OutOfModbusBlockError, ModbusInvalidResponseError,
|
|
|
+ ModbusInvalidRequestError
|
|
|
+)
|
|
|
+from utils.modbus.hooks import call_hooks
|
|
|
+from utils.modbus.utils import threadsafe_function, get_log_buffer
|
|
|
+
|
|
|
+
|
|
|
+class Query(object):
|
|
|
+ def __init__(self):
|
|
|
+ pass
|
|
|
+
|
|
|
+ def build_request(self, pdu, slave):
|
|
|
+ raise NotImplementedError()
|
|
|
+
|
|
|
+ def parse_response(self, response):
|
|
|
+ raise NotImplementedError()
|
|
|
+
|
|
|
+ def parse_request(self, request):
|
|
|
+ raise NotImplementedError()
|
|
|
+
|
|
|
+ def build_response(self, response_pdu):
|
|
|
+ raise NotImplementedError()
|
|
|
+
|
|
|
+
|
|
|
+class Master(object):
|
|
|
+ def __init__(self, timeout_in_sec, hooks=None):
|
|
|
+ self._timeout = timeout_in_sec
|
|
|
+ self._verbose = False
|
|
|
+ self._is_opened = False
|
|
|
+
|
|
|
+ def __del__(self):
|
|
|
+ self.close()
|
|
|
+
|
|
|
+ def set_verbose(self, verbose):
|
|
|
+ self._verbose = verbose
|
|
|
+
|
|
|
+ def open(self):
|
|
|
+ if not self._is_opened:
|
|
|
+ self._do_open()
|
|
|
+ self._is_opened = True
|
|
|
+
|
|
|
+ def close(self):
|
|
|
+ if self._is_opened:
|
|
|
+ ret = self._do_close()
|
|
|
+ if ret:
|
|
|
+ self._is_opened = False
|
|
|
+
|
|
|
+ def _do_open(self):
|
|
|
+ raise NotImplementedError()
|
|
|
+
|
|
|
+ def _do_close(self):
|
|
|
+ raise NotImplementedError()
|
|
|
+
|
|
|
+ def _send(self, buf):
|
|
|
+ raise NotImplementedError()
|
|
|
+
|
|
|
+ def _recv(self, expected_length):
|
|
|
+ raise NotImplementedError()
|
|
|
+
|
|
|
+ def _make_query(self):
|
|
|
+ raise NotImplementedError()
|
|
|
+
|
|
|
+ @threadsafe_function
|
|
|
+ def execute(
|
|
|
+ self, slave, function_code, starting_address, quantity_of_x=0, output_value=0, data_format="",
|
|
|
+ expected_length=-1, write_starting_address_fc23=0, number_file=None, pdu=""
|
|
|
+ ):
|
|
|
+ """
|
|
|
+ Execute a modbus query and returns the data part of the answer as a tuple
|
|
|
+ The returned tuple depends on the query function code. see modbus protocol
|
|
|
+ specification for details
|
|
|
+ data_format makes possible to extract the data like defined in the
|
|
|
+ struct python module documentation
|
|
|
+ For function Read_File_Record
|
|
|
+ starting_address, quantity_of_x, number_file must be tuple ()
|
|
|
+ of one long (by the number of requested sub_seq)
|
|
|
+ the result will be
|
|
|
+ ((sub _ seq_0 _ data), (sub_seq_1_data),... (sub_seq_N_data)).
|
|
|
+ """
|
|
|
+
|
|
|
+ is_read_function = False
|
|
|
+ nb_of_digits = 0
|
|
|
+ if number_file is None:
|
|
|
+ number_file = tuple()
|
|
|
+
|
|
|
+ # open the connection if it is not already done
|
|
|
+ self.open()
|
|
|
+
|
|
|
+ # Build the modbus pdu and the format of the expected data.
|
|
|
+ # It depends of function code. see modbus specifications for details.
|
|
|
+ if function_code == defines.READ_COILS or function_code == defines.READ_DISCRETE_INPUTS:
|
|
|
+ is_read_function = True
|
|
|
+ pdu = struct.pack(">BHH", function_code, starting_address, quantity_of_x)
|
|
|
+ byte_count = quantity_of_x // 8
|
|
|
+ if (quantity_of_x % 8) > 0:
|
|
|
+ byte_count += 1
|
|
|
+ nb_of_digits = quantity_of_x
|
|
|
+ if not data_format:
|
|
|
+ data_format = ">" + (byte_count * "B")
|
|
|
+ if expected_length < 0:
|
|
|
+ # No length was specified and calculated length can be used:
|
|
|
+ # slave + func + bytcodeLen + bytecode + crc1 + crc2
|
|
|
+ expected_length = byte_count + 5
|
|
|
+
|
|
|
+ elif function_code == defines.READ_INPUT_REGISTERS or function_code == defines.READ_HOLDING_REGISTERS:
|
|
|
+ is_read_function = True
|
|
|
+ pdu = struct.pack(">BHH", function_code, starting_address, quantity_of_x)
|
|
|
+ if not data_format:
|
|
|
+ data_format = ">" + (quantity_of_x * "H")
|
|
|
+ if expected_length < 0:
|
|
|
+ # No length was specified and calculated length can be used:
|
|
|
+ # slave + func + bytcodeLen + bytecode x 2 + crc1 + crc2
|
|
|
+ expected_length = 2 * quantity_of_x + 5
|
|
|
+
|
|
|
+ elif function_code == defines.READ_FILE_RECORD:
|
|
|
+ is_read_function = True
|
|
|
+ if (
|
|
|
+ isinstance(number_file, tuple)
|
|
|
+ and isinstance(starting_address, tuple)
|
|
|
+ and isinstance(quantity_of_x, tuple)
|
|
|
+ and len(number_file) == len(starting_address) == len(quantity_of_x) > 0
|
|
|
+ ):
|
|
|
+ count_seq = len(number_file)
|
|
|
+ else:
|
|
|
+ raise ModbusInvalidRequestError(
|
|
|
+ 'For function READ_FILE_RECORD param'
|
|
|
+ 'starting_address, quantity_of_x, number_file must be tuple()'
|
|
|
+ 'of one length > 0 (by the number of requested sub_seq)'
|
|
|
+ )
|
|
|
+ pdu = struct.pack(">BB", function_code, count_seq * 7) + b''.join(map(lambda zip_param: struct.pack(">BHHH", *zip_param), zip(count_seq * (6, ), number_file, starting_address, quantity_of_x)))
|
|
|
+ if not data_format:
|
|
|
+ data_format = ">BB" + 'BB'.join(map(lambda x: x * 'H', quantity_of_x))
|
|
|
+ if expected_length < 0:
|
|
|
+ # No length was specified and calculated length can be used:
|
|
|
+ # slave + func + bytcodeLen + (byteLenSubReq+byteref+bytecode[] x 2)*countSubReq + crc1 + crc2
|
|
|
+ expected_length = 2 * sum(quantity_of_x) + 2 * count_seq + 5
|
|
|
+
|
|
|
+ elif (function_code == defines.WRITE_SINGLE_COIL) or (function_code == defines.WRITE_SINGLE_REGISTER):
|
|
|
+ if function_code == defines.WRITE_SINGLE_COIL:
|
|
|
+ if output_value != 0:
|
|
|
+ output_value = 0xff00
|
|
|
+ fmt = ">BHH"
|
|
|
+ else:
|
|
|
+ fmt = ">BH" + ("H" if output_value >= 0 else "h")
|
|
|
+ pdu = struct.pack(fmt, function_code, starting_address, output_value)
|
|
|
+ if not data_format:
|
|
|
+ data_format = ">HH"
|
|
|
+ if expected_length < 0:
|
|
|
+ # No length was specified and calculated length can be used:
|
|
|
+ # slave + func + adress1 + adress2 + value1+value2 + crc1 + crc2
|
|
|
+ expected_length = 8
|
|
|
+
|
|
|
+ elif function_code == defines.WRITE_MULTIPLE_COILS:
|
|
|
+ byte_count = len(output_value) // 8
|
|
|
+ if (len(output_value) % 8) > 0:
|
|
|
+ byte_count += 1
|
|
|
+ pdu = struct.pack(">BHHB", function_code, starting_address, len(output_value), byte_count)
|
|
|
+ i, byte_value = 0, 0
|
|
|
+ for j in output_value:
|
|
|
+ if j > 0:
|
|
|
+ byte_value += pow(2, i)
|
|
|
+ if i == 7:
|
|
|
+ pdu += struct.pack(">B", byte_value)
|
|
|
+ i, byte_value = 0, 0
|
|
|
+ else:
|
|
|
+ i += 1
|
|
|
+ if i > 0:
|
|
|
+ pdu += struct.pack(">B", byte_value)
|
|
|
+ if not data_format:
|
|
|
+ data_format = ">HH"
|
|
|
+ if expected_length < 0:
|
|
|
+ # No length was specified and calculated length can be used:
|
|
|
+ # slave + func + adress1 + adress2 + outputQuant1 + outputQuant2 + crc1 + crc2
|
|
|
+ expected_length = 8
|
|
|
+
|
|
|
+ elif function_code == defines.WRITE_MULTIPLE_REGISTERS:
|
|
|
+ if output_value and data_format:
|
|
|
+ byte_count = struct.calcsize(data_format)
|
|
|
+ else:
|
|
|
+ byte_count = 2 * len(output_value)
|
|
|
+ pdu = struct.pack(">BHHB", function_code, starting_address, byte_count // 2, byte_count)
|
|
|
+ if output_value and data_format:
|
|
|
+ pdu += struct.pack(data_format, *output_value)
|
|
|
+ else:
|
|
|
+ for j in output_value:
|
|
|
+ fmt = "H" if j >= 0 else "h"
|
|
|
+ pdu += struct.pack(">" + fmt, j)
|
|
|
+ # data_format is now used to process response which is always 2 registers:
|
|
|
+ # 1) data address of first register, 2) number of registers written
|
|
|
+ data_format = ">HH"
|
|
|
+ if expected_length < 0:
|
|
|
+ # No length was specified and calculated length can be used:
|
|
|
+ # slave + func + adress1 + adress2 + outputQuant1 + outputQuant2 + crc1 + crc2
|
|
|
+ expected_length = 8
|
|
|
+
|
|
|
+ elif function_code == defines.READ_EXCEPTION_STATUS:
|
|
|
+ pdu = struct.pack(">B", function_code)
|
|
|
+ data_format = ">B"
|
|
|
+ if expected_length < 0:
|
|
|
+ # No length was specified and calculated length can be used:
|
|
|
+ expected_length = 5
|
|
|
+
|
|
|
+ elif function_code == defines.DIAGNOSTIC:
|
|
|
+ # SubFuncCode are in starting_address
|
|
|
+ pdu = struct.pack(">BH", function_code, starting_address)
|
|
|
+ if len(output_value) > 0:
|
|
|
+ for j in output_value:
|
|
|
+ # copy data in pdu
|
|
|
+ pdu += struct.pack(">B", j)
|
|
|
+ if not data_format:
|
|
|
+ data_format = ">" + (len(output_value) * "B")
|
|
|
+ if expected_length < 0:
|
|
|
+ # No length was specified and calculated length can be used:
|
|
|
+ # slave + func + SubFunc1 + SubFunc2 + Data + crc1 + crc2
|
|
|
+ expected_length = len(output_value) + 6
|
|
|
+
|
|
|
+ elif function_code == defines.READ_WRITE_MULTIPLE_REGISTERS:
|
|
|
+ is_read_function = True
|
|
|
+ byte_count = 2 * len(output_value)
|
|
|
+ pdu = struct.pack(
|
|
|
+ ">BHHHHB",
|
|
|
+ function_code, starting_address, quantity_of_x, write_starting_address_fc23,
|
|
|
+ len(output_value), byte_count
|
|
|
+ )
|
|
|
+ for j in output_value:
|
|
|
+ fmt = "H" if j >= 0 else "h"
|
|
|
+ # copy data in pdu
|
|
|
+ pdu += struct.pack(">" + fmt, j)
|
|
|
+ if not data_format:
|
|
|
+ data_format = ">" + (quantity_of_x * "H")
|
|
|
+ if expected_length < 0:
|
|
|
+ # No length was specified and calculated length can be used:
|
|
|
+ # slave + func + bytcodeLen + bytecode x 2 + crc1 + crc2
|
|
|
+ expected_length = 2 * quantity_of_x + 5
|
|
|
+
|
|
|
+ elif function_code == defines.RAW:
|
|
|
+ # caller has to set arguments "pdu", "expected_length", and "data_format"
|
|
|
+ pass
|
|
|
+
|
|
|
+ elif function_code == defines.DEVICE_INFO:
|
|
|
+ # is_read_function = True
|
|
|
+ mei_type = 0x0E
|
|
|
+ pdu = struct.pack(
|
|
|
+ ">BBBB",
|
|
|
+ # function_code = 43 (0x2B)
|
|
|
+ # MEI Type = 0x0E (Read Device Identification)
|
|
|
+ # output_value[0] = Read Device ID code
|
|
|
+ # output_value[1] = Object Id
|
|
|
+ function_code, mei_type, output_value[0], output_value[1]
|
|
|
+ )
|
|
|
+
|
|
|
+ else:
|
|
|
+ raise ModbusFunctionNotSupportedError("The {0} function code is not supported. ".format(function_code))
|
|
|
+
|
|
|
+ # instantiate a query which implements the MAC (TCP or RTU) part of the protocol
|
|
|
+ query = self._make_query()
|
|
|
+
|
|
|
+ # add the mac part of the protocol to the request
|
|
|
+ request = query.build_request(pdu, slave)
|
|
|
+
|
|
|
+ # send the request to the slave
|
|
|
+ retval = call_hooks("modbus.Master.before_send", (self, request))
|
|
|
+ if retval is not None:
|
|
|
+ request = retval
|
|
|
+ if self._verbose:
|
|
|
+ LOGGER.debug(get_log_buffer("-> ", request))
|
|
|
+ self._send(request)
|
|
|
+
|
|
|
+ call_hooks("modbus.Master.after_send", (self, ))
|
|
|
+
|
|
|
+ if slave != 0:
|
|
|
+ # receive the data from the slave
|
|
|
+ response = self._recv(expected_length)
|
|
|
+ retval = call_hooks("modbus.Master.after_recv", (self, response))
|
|
|
+ if retval is not None:
|
|
|
+ response = retval
|
|
|
+ if self._verbose:
|
|
|
+ LOGGER.debug(get_log_buffer("<- ", response))
|
|
|
+
|
|
|
+ # extract the pdu part of the response
|
|
|
+ response_pdu = query.parse_response(response)
|
|
|
+
|
|
|
+ # analyze the received data
|
|
|
+ (return_code, byte_2) = struct.unpack(">BB", response_pdu[0:2])
|
|
|
+
|
|
|
+ if return_code > 0x80:
|
|
|
+ # the slave has returned an error
|
|
|
+ exception_code = byte_2
|
|
|
+ raise ModbusError(exception_code)
|
|
|
+ else:
|
|
|
+ if is_read_function:
|
|
|
+ # get the values returned by the reading function
|
|
|
+ byte_count = byte_2
|
|
|
+ data = response_pdu[2:]
|
|
|
+ if byte_count != len(data):
|
|
|
+ # the byte count in the pdu is invalid
|
|
|
+ raise ModbusInvalidResponseError(
|
|
|
+ "Byte count is {0} while actual number of bytes is {1}. ".format(byte_count, len(data))
|
|
|
+ )
|
|
|
+ elif function_code == defines.DEVICE_INFO:
|
|
|
+ data = response_pdu[1:]
|
|
|
+ data_format = ">" + (len(data) * "B")
|
|
|
+ else:
|
|
|
+ # returns what is returned by the slave after a writing function
|
|
|
+ data = response_pdu[1:]
|
|
|
+
|
|
|
+ # returns the data as a tuple according to the data_format
|
|
|
+ # (calculated based on the function or user-defined)
|
|
|
+ if (re.match("[>]?[sp]?", data_format)):
|
|
|
+ # result = data.decode()
|
|
|
+ result = data
|
|
|
+ else:
|
|
|
+ result = struct.unpack(data_format, data)
|
|
|
+ if nb_of_digits > 0:
|
|
|
+ digits = []
|
|
|
+ for byte_val in result:
|
|
|
+ for i in range(8):
|
|
|
+ if len(digits) >= nb_of_digits:
|
|
|
+ break
|
|
|
+ digits.append(byte_val % 2)
|
|
|
+ byte_val = byte_val >> 1
|
|
|
+ result = tuple(digits)
|
|
|
+ if function_code == defines.READ_FILE_RECORD:
|
|
|
+ sub_seq = list()
|
|
|
+ ptr = 0
|
|
|
+ while ptr < len(result):
|
|
|
+ sub_seq += ((ptr + 2, ptr + 2 + result[ptr] // 2), )
|
|
|
+ ptr += result[ptr] // 2 + 2
|
|
|
+ result = tuple(map(lambda sub_seq_x: result[sub_seq_x[0]:sub_seq_x[1]], sub_seq))
|
|
|
+ return result
|
|
|
+
|
|
|
+ def set_timeout(self, timeout_in_sec):
|
|
|
+ """Defines a timeout on the MAC layer"""
|
|
|
+ self._timeout = timeout_in_sec
|
|
|
+
|
|
|
+ def get_timeout(self):
|
|
|
+ """Gets the current value of the MAC layer timeout"""
|
|
|
+ return self._timeout
|
|
|
+
|
|
|
+
|
|
|
+class ModbusBlock(object):
|
|
|
+ def __init__(self, starting_address, size, name=''):
|
|
|
+ self.starting_address = starting_address
|
|
|
+ self._data = [0] * size
|
|
|
+ self.size = len(self._data)
|
|
|
+
|
|
|
+ def is_in(self, starting_address, size):
|
|
|
+ if starting_address > self.starting_address:
|
|
|
+ return (self.starting_address + self.size) > starting_address
|
|
|
+ elif starting_address < self.starting_address:
|
|
|
+ return (starting_address + size) > self.starting_address
|
|
|
+ return True
|
|
|
+
|
|
|
+ def __getitem__(self, item):
|
|
|
+ return self._data.__getitem__(item)
|
|
|
+
|
|
|
+ def __setitem__(self, item, value):
|
|
|
+ call_hooks("modbus.ModbusBlock.setitem", (self, item, value))
|
|
|
+ return self._data.__setitem__(item, value)
|
|
|
+
|
|
|
+
|
|
|
+class Slave(object):
|
|
|
+ def __init__(self, slave_id, unsigned=True, memory=None):
|
|
|
+ self._id = slave_id
|
|
|
+ self.unsigned = unsigned
|
|
|
+ self._blocks = {}
|
|
|
+
|
|
|
+ if memory is None:
|
|
|
+ self._memory = {
|
|
|
+ defines.COILS: [],
|
|
|
+ defines.DISCRETE_INPUTS: [],
|
|
|
+ defines.HOLDING_REGISTERS: [],
|
|
|
+ defines.ANALOG_INPUTS: [],
|
|
|
+ }
|
|
|
+ else:
|
|
|
+ self._memory = memory
|
|
|
+
|
|
|
+ self._data_lock = threading.RLock()
|
|
|
+
|
|
|
+ self._fn_code_map = {
|
|
|
+ defines.READ_COILS: self._read_coils,
|
|
|
+ defines.READ_DISCRETE_INPUTS: self._read_discrete_inputs,
|
|
|
+ defines.READ_INPUT_REGISTERS: self._read_input_registers,
|
|
|
+ defines.READ_HOLDING_REGISTERS: self._read_holding_registers,
|
|
|
+ defines.READ_EXCEPTION_STATUS: self._read_exception_status,
|
|
|
+ defines.WRITE_SINGLE_COIL: self._write_single_coil,
|
|
|
+ defines.WRITE_SINGLE_REGISTER: self._write_single_register,
|
|
|
+ defines.WRITE_MULTIPLE_COILS: self._write_multiple_coils,
|
|
|
+ defines.WRITE_MULTIPLE_REGISTERS: self._write_multiple_registers,
|
|
|
+ defines.READ_WRITE_MULTIPLE_REGISTERS: self._read_write_multiple_registers,
|
|
|
+ }
|
|
|
+
|
|
|
+ def _get_block_and_offset(self, block_type, address, length):
|
|
|
+ for block in self._memory[block_type]:
|
|
|
+ if address >= block.starting_address:
|
|
|
+ offset = address - block.starting_address
|
|
|
+ if block.size >= offset + length:
|
|
|
+ return block, offset
|
|
|
+ raise ModbusError(defines.ILLEGAL_DATA_ADDRESS)
|
|
|
+
|
|
|
+ def _read_digital(self, block_type, request_pdu):
|
|
|
+ (starting_address, quantity_of_x) = struct.unpack(">HH", request_pdu[1:5])
|
|
|
+
|
|
|
+ if (quantity_of_x <= 0) or (quantity_of_x > 2000):
|
|
|
+ # maximum allowed size is 2000 bits in one reading
|
|
|
+ raise ModbusError(defines.ILLEGAL_DATA_VALUE)
|
|
|
+
|
|
|
+ block, offset = self._get_block_and_offset(block_type, starting_address, quantity_of_x)
|
|
|
+
|
|
|
+ values = block[offset: offset + quantity_of_x]
|
|
|
+
|
|
|
+ # pack bits in bytes
|
|
|
+ byte_count = quantity_of_x // 8
|
|
|
+ if (quantity_of_x % 8) > 0:
|
|
|
+ byte_count += 1
|
|
|
+
|
|
|
+ # write the response header
|
|
|
+ response = struct.pack(">B", byte_count)
|
|
|
+
|
|
|
+ i, byte_value = 0, 0
|
|
|
+ for coil in values:
|
|
|
+ if coil:
|
|
|
+ byte_value += (1 << i)
|
|
|
+ if i >= 7:
|
|
|
+ # write the values of 8 bits in a byte
|
|
|
+ response += struct.pack(">B", byte_value)
|
|
|
+ # reset the counters
|
|
|
+ i, byte_value = 0, 0
|
|
|
+ else:
|
|
|
+ i += 1
|
|
|
+
|
|
|
+ # if there is remaining bits: add one more byte with their values
|
|
|
+ if i > 0:
|
|
|
+ fmt = "B" if self.unsigned else "b"
|
|
|
+ response += struct.pack(">" + fmt, byte_value)
|
|
|
+ return response
|
|
|
+
|
|
|
+ def _read_coils(self, request_pdu):
|
|
|
+ """handle read coils modbus function"""
|
|
|
+ call_hooks("modbus.Slave.handle_read_coils_request", (self, request_pdu))
|
|
|
+ return self._read_digital(defines.COILS, request_pdu)
|
|
|
+
|
|
|
+ def _read_discrete_inputs(self, request_pdu):
|
|
|
+ """handle read discrete inputs modbus function"""
|
|
|
+ call_hooks("modbus.Slave.handle_read_discrete_inputs_request", (self, request_pdu))
|
|
|
+ return self._read_digital(defines.DISCRETE_INPUTS, request_pdu)
|
|
|
+
|
|
|
+ def _read_registers(self, block_type, request_pdu):
|
|
|
+ """read the value of holding and input registers"""
|
|
|
+ (starting_address, quantity_of_x) = struct.unpack(">HH", request_pdu[1:5])
|
|
|
+
|
|
|
+ if (quantity_of_x <= 0) or (quantity_of_x > 125):
|
|
|
+ # maximum allowed size is 125 registers in one reading
|
|
|
+ LOGGER.debug("quantity_of_x is %d", quantity_of_x)
|
|
|
+ raise ModbusError(defines.ILLEGAL_DATA_VALUE)
|
|
|
+
|
|
|
+ # look for the block corresponding to the request
|
|
|
+ block, offset = self._get_block_and_offset(block_type, starting_address, quantity_of_x)
|
|
|
+
|
|
|
+ # get the values
|
|
|
+ values = block[offset: offset + quantity_of_x]
|
|
|
+
|
|
|
+ # write the response header
|
|
|
+ response = struct.pack(">B", 2 * quantity_of_x)
|
|
|
+ # add the values of every register on 2 bytes
|
|
|
+ for reg in values:
|
|
|
+ fmt = "H" if self.unsigned else "h"
|
|
|
+ response += struct.pack(">" + fmt, reg)
|
|
|
+ return response
|
|
|
+
|
|
|
+ def _read_holding_registers(self, request_pdu):
|
|
|
+ """handle read coils modbus function"""
|
|
|
+ call_hooks("modbus.Slave.handle_read_holding_registers_request", (self, request_pdu))
|
|
|
+ return self._read_registers(defines.HOLDING_REGISTERS, request_pdu)
|
|
|
+
|
|
|
+ def _read_input_registers(self, request_pdu):
|
|
|
+ """handle read coils modbus function"""
|
|
|
+ call_hooks("modbus.Slave.handle_read_input_registers_request", (self, request_pdu))
|
|
|
+ return self._read_registers(defines.ANALOG_INPUTS, request_pdu)
|
|
|
+
|
|
|
+ def _read_exception_status(self, request_pdu):
|
|
|
+ """handle read exception status modbus function"""
|
|
|
+ call_hooks("modbus.Slave.handle_read_exception_status_request", (self, request_pdu))
|
|
|
+ response = struct.pack(">B", 0)
|
|
|
+ return response
|
|
|
+
|
|
|
+ def _read_write_multiple_registers(self, request_pdu):
|
|
|
+ """execute modbus function 23"""
|
|
|
+ call_hooks("modbus.Slave.handle_read_write_multiple_registers_request", (self, request_pdu))
|
|
|
+ # get the starting address and the number of items from the request pdu
|
|
|
+ (starting_read_address, quantity_of_x_to_read, starting_write_address, quantity_of_x_to_write, byte_count_to_write) = struct.unpack(">HHHHB", request_pdu[1:10])
|
|
|
+
|
|
|
+ # read part
|
|
|
+ if (quantity_of_x_to_read <= 0) or (quantity_of_x_to_read > 125):
|
|
|
+ # maximum allowed size is 125 registers in one reading
|
|
|
+ LOGGER.debug("quantity_of_x_to_read is %d", quantity_of_x_to_read)
|
|
|
+ raise ModbusError(defines.ILLEGAL_DATA_VALUE)
|
|
|
+
|
|
|
+ # look for the block corresponding to the request
|
|
|
+ block, offset = self._get_block_and_offset(defines.HOLDING_REGISTERS, starting_read_address, quantity_of_x_to_read)
|
|
|
+
|
|
|
+ # get the values
|
|
|
+ values = block[offset: offset + quantity_of_x_to_read]
|
|
|
+ # write the response header
|
|
|
+ response = struct.pack(">B", 2 * quantity_of_x_to_read)
|
|
|
+ # add the values of every register on 2 bytes
|
|
|
+ for reg in values:
|
|
|
+ fmt = "H" if self.unsigned else "h"
|
|
|
+ response += struct.pack(">" + fmt, reg)
|
|
|
+
|
|
|
+ # write part
|
|
|
+ if (quantity_of_x_to_write <= 0) or (quantity_of_x_to_write > 123) or (byte_count_to_write != (quantity_of_x_to_write * 2)):
|
|
|
+ # maximum allowed size is 123 registers in one reading
|
|
|
+ raise ModbusError(defines.ILLEGAL_DATA_VALUE)
|
|
|
+
|
|
|
+ # look for the block corresponding to the request
|
|
|
+ block, offset = self._get_block_and_offset(defines.HOLDING_REGISTERS, starting_write_address, quantity_of_x_to_write)
|
|
|
+
|
|
|
+ count = 0
|
|
|
+ for i in range(quantity_of_x_to_write):
|
|
|
+ count += 1
|
|
|
+ fmt = "H" if self.unsigned else "h"
|
|
|
+ block[offset + i] = struct.unpack(">" + fmt, request_pdu[10 + 2 * i:12 + 2 * i])[0]
|
|
|
+
|
|
|
+ return response
|
|
|
+
|
|
|
+ def _write_multiple_registers(self, request_pdu):
|
|
|
+ """execute modbus function 16"""
|
|
|
+ call_hooks("modbus.Slave.handle_write_multiple_registers_request", (self, request_pdu))
|
|
|
+ # get the starting address and the number of items from the request pdu
|
|
|
+ (starting_address, quantity_of_x, byte_count) = struct.unpack(">HHB", request_pdu[1:6])
|
|
|
+
|
|
|
+ if (quantity_of_x <= 0) or (quantity_of_x > 123) or (byte_count != (quantity_of_x * 2)):
|
|
|
+ # maximum allowed size is 123 registers in one reading
|
|
|
+ raise ModbusError(defines.ILLEGAL_DATA_VALUE)
|
|
|
+
|
|
|
+ # look for the block corresponding to the request
|
|
|
+ block, offset = self._get_block_and_offset(defines.HOLDING_REGISTERS, starting_address, quantity_of_x)
|
|
|
+
|
|
|
+ count = 0
|
|
|
+ for i in range(quantity_of_x):
|
|
|
+ count += 1
|
|
|
+ fmt = "H" if self.unsigned else "h"
|
|
|
+ block[offset + i] = struct.unpack(">" + fmt, request_pdu[6 + 2 * i: 8 + 2 * i])[0]
|
|
|
+
|
|
|
+ return struct.pack(">HH", starting_address, count)
|
|
|
+
|
|
|
+ def _write_multiple_coils(self, request_pdu):
|
|
|
+ """execute modbus function 15"""
|
|
|
+ call_hooks("modbus.Slave.handle_write_multiple_coils_request", (self, request_pdu))
|
|
|
+ # get the starting address and the number of items from the request pdu
|
|
|
+ (starting_address, quantity_of_x, byte_count) = struct.unpack(">HHB", request_pdu[1:6])
|
|
|
+
|
|
|
+ expected_byte_count = quantity_of_x // 8
|
|
|
+ if (quantity_of_x % 8) > 0:
|
|
|
+ expected_byte_count += 1
|
|
|
+
|
|
|
+ if (quantity_of_x <= 0) or (quantity_of_x > 1968) or (byte_count != expected_byte_count):
|
|
|
+ # maximum allowed size is 1968 coils
|
|
|
+ raise ModbusError(defines.ILLEGAL_DATA_VALUE)
|
|
|
+
|
|
|
+ # look for the block corresponding to the request
|
|
|
+ block, offset = self._get_block_and_offset(defines.COILS, starting_address, quantity_of_x)
|
|
|
+
|
|
|
+ count = 0
|
|
|
+ for i in range(byte_count):
|
|
|
+ if count >= quantity_of_x:
|
|
|
+ break
|
|
|
+ fmt = "B" if self.unsigned else "b"
|
|
|
+ (byte_value, ) = struct.unpack(">" + fmt, request_pdu[6 + i: 7 + i])
|
|
|
+ for j in range(8):
|
|
|
+ if count >= quantity_of_x:
|
|
|
+ break
|
|
|
+
|
|
|
+ if byte_value & (1 << j):
|
|
|
+ block[offset + i * 8 + j] = 1
|
|
|
+ else:
|
|
|
+ block[offset + i * 8 + j] = 0
|
|
|
+
|
|
|
+ count += 1
|
|
|
+ return struct.pack(">HH", starting_address, count)
|
|
|
+
|
|
|
+ def _write_single_register(self, request_pdu):
|
|
|
+ """execute modbus function 6"""
|
|
|
+ call_hooks("modbus.Slave.handle_write_single_register_request", (self, request_pdu))
|
|
|
+
|
|
|
+ fmt = "H" if self.unsigned else "h"
|
|
|
+ (data_address, value) = struct.unpack(">H" + fmt, request_pdu[1:5])
|
|
|
+ block, offset = self._get_block_and_offset(defines.HOLDING_REGISTERS, data_address, 1)
|
|
|
+ block[offset] = value
|
|
|
+ # returns echo of the command
|
|
|
+ return request_pdu[1:]
|
|
|
+
|
|
|
+ def _write_single_coil(self, request_pdu):
|
|
|
+ """execute modbus function 5"""
|
|
|
+
|
|
|
+ call_hooks("modbus.Slave.handle_write_single_coil_request", (self, request_pdu))
|
|
|
+ (data_address, value) = struct.unpack(">HH", request_pdu[1:5])
|
|
|
+ block, offset = self._get_block_and_offset(defines.COILS, data_address, 1)
|
|
|
+ if value == 0:
|
|
|
+ block[offset] = 0
|
|
|
+ elif value == 0xff00:
|
|
|
+ block[offset] = 1
|
|
|
+ else:
|
|
|
+ raise ModbusError(defines.ILLEGAL_DATA_VALUE)
|
|
|
+ # returns echo of the command
|
|
|
+ return request_pdu[1:]
|
|
|
+
|
|
|
+ def handle_request(self, request_pdu, broadcast=False):
|
|
|
+ """
|
|
|
+ parse the request pdu, makes the corresponding action
|
|
|
+ and returns the response pdu
|
|
|
+ """
|
|
|
+ # thread-safe
|
|
|
+ with self._data_lock:
|
|
|
+ try:
|
|
|
+ retval = call_hooks("modbus.Slave.handle_request", (self, request_pdu))
|
|
|
+ if retval is not None:
|
|
|
+ return retval
|
|
|
+
|
|
|
+ # get the function code
|
|
|
+ (function_code, ) = struct.unpack(">B", request_pdu[0:1])
|
|
|
+
|
|
|
+ # check if the function code is valid. If not returns error response
|
|
|
+ if function_code not in self._fn_code_map:
|
|
|
+ raise ModbusError(defines.ILLEGAL_FUNCTION)
|
|
|
+
|
|
|
+ # if read query is broadcasted raises an error
|
|
|
+ cant_be_broadcasted = (
|
|
|
+ defines.READ_COILS,
|
|
|
+ defines.READ_DISCRETE_INPUTS,
|
|
|
+ defines.READ_INPUT_REGISTERS,
|
|
|
+ defines.READ_HOLDING_REGISTERS
|
|
|
+ )
|
|
|
+ if broadcast and (function_code in cant_be_broadcasted):
|
|
|
+ raise ModbusInvalidRequestError("Function %d can not be broadcasted" % function_code)
|
|
|
+
|
|
|
+ # execute the corresponding function
|
|
|
+ response_pdu = self._fn_code_map[function_code](request_pdu)
|
|
|
+ if response_pdu:
|
|
|
+ if broadcast:
|
|
|
+ call_hooks("modbus.Slave.on_handle_broadcast", (self, response_pdu))
|
|
|
+ LOGGER.debug("broadcast: %s", get_log_buffer("!!", response_pdu))
|
|
|
+ return ""
|
|
|
+ else:
|
|
|
+ return struct.pack(">B", function_code) + response_pdu
|
|
|
+ raise Exception("No response for function %d" % function_code)
|
|
|
+
|
|
|
+ except ModbusError as excpt:
|
|
|
+ LOGGER.debug(str(excpt))
|
|
|
+ call_hooks("modbus.Slave.on_exception", (self, function_code, excpt))
|
|
|
+ return struct.pack(">BB", function_code + 128, excpt.get_exception_code())
|
|
|
+
|
|
|
+ def add_block(self, block_name, block_type, starting_address, size):
|
|
|
+ """Add a new block identified by its name"""
|
|
|
+ # thread-safe
|
|
|
+ with self._data_lock:
|
|
|
+ if size <= 0:
|
|
|
+ raise InvalidArgumentError("size must be a positive number")
|
|
|
+
|
|
|
+ if starting_address < 0:
|
|
|
+ raise InvalidArgumentError("starting address must be zero or positive number")
|
|
|
+
|
|
|
+ if block_name in self._blocks:
|
|
|
+ raise DuplicatedKeyError("Block {0} already exists. ".format(block_name))
|
|
|
+
|
|
|
+ if block_type not in self._memory:
|
|
|
+ raise InvalidModbusBlockError("Invalid block type {0}".format(block_type))
|
|
|
+
|
|
|
+ # check that the new block doesn't overlap an existing block
|
|
|
+ # it means that only 1 block per type must correspond to a given address
|
|
|
+ # for example: it must not have 2 holding registers at address 100
|
|
|
+ index = 0
|
|
|
+ for i in range(len(self._memory[block_type])):
|
|
|
+ block = self._memory[block_type][i]
|
|
|
+ if block.is_in(starting_address, size):
|
|
|
+ raise OverlapModbusBlockError(
|
|
|
+ "Overlap block at {0} size {1}".format(block.starting_address, block.size)
|
|
|
+ )
|
|
|
+ if block.starting_address > starting_address:
|
|
|
+ index = i
|
|
|
+ break
|
|
|
+
|
|
|
+ # if the block is ok: register it
|
|
|
+ self._blocks[block_name] = (block_type, starting_address)
|
|
|
+ # add it in the 'per type' shortcut
|
|
|
+ self._memory[block_type].insert(index, ModbusBlock(starting_address, size, block_name))
|
|
|
+
|
|
|
+ def remove_block(self, block_name):
|
|
|
+ """
|
|
|
+ Remove the block with the given name.
|
|
|
+ Raise an exception if not found
|
|
|
+ """
|
|
|
+ # thread safe
|
|
|
+ with self._data_lock:
|
|
|
+ block = self._get_block(block_name)
|
|
|
+
|
|
|
+ # the block has been found: remove it from the shortcut
|
|
|
+ block_type = self._blocks.pop(block_name)[0]
|
|
|
+ self._memory[block_type].remove(block)
|
|
|
+
|
|
|
+ def remove_all_blocks(self):
|
|
|
+ """
|
|
|
+ Remove all the blocks
|
|
|
+ """
|
|
|
+ # thread safe
|
|
|
+ with self._data_lock:
|
|
|
+ self._blocks.clear()
|
|
|
+ for key in self._memory:
|
|
|
+ self._memory[key] = []
|
|
|
+
|
|
|
+ def _get_block(self, block_name):
|
|
|
+ """Find a block by its name and raise and exception if not found"""
|
|
|
+ if block_name not in self._blocks:
|
|
|
+ raise MissingKeyError("block {0} not found".format(block_name))
|
|
|
+ (block_type, starting_address) = self._blocks[block_name]
|
|
|
+ for block in self._memory[block_type]:
|
|
|
+ if block.starting_address == starting_address:
|
|
|
+ return block
|
|
|
+ raise Exception("Bug?: the block {0} is not registered properly in memory".format(block_name))
|
|
|
+
|
|
|
+ def set_values(self, block_name, address, values):
|
|
|
+ """
|
|
|
+ Set the values of the items at the given address
|
|
|
+ If values is a list or a tuple, the value of every item is written
|
|
|
+ If values is a number, only one value is written
|
|
|
+ """
|
|
|
+ # thread safe
|
|
|
+ with self._data_lock:
|
|
|
+ block = self._get_block(block_name)
|
|
|
+
|
|
|
+ # the block has been found
|
|
|
+ # check that it doesn't write out of the block
|
|
|
+ offset = address - block.starting_address
|
|
|
+
|
|
|
+ size = 1
|
|
|
+ if isinstance(values, list) or isinstance(values, tuple):
|
|
|
+ size = len(values)
|
|
|
+
|
|
|
+ if (offset < 0) or ((offset + size) > block.size):
|
|
|
+ raise OutOfModbusBlockError(
|
|
|
+ "address {0} size {1} is out of block {2}".format(address, size, block_name)
|
|
|
+ )
|
|
|
+
|
|
|
+ # if Ok: write the values
|
|
|
+ if isinstance(values, list) or isinstance(values, tuple):
|
|
|
+ block[offset: offset + len(values)] = values
|
|
|
+ else:
|
|
|
+ block[offset] = values
|
|
|
+
|
|
|
+ def get_values(self, block_name, address, size=1):
|
|
|
+ """
|
|
|
+ return the values of n items at the given address of the given block
|
|
|
+ """
|
|
|
+ # thread safe
|
|
|
+ with self._data_lock:
|
|
|
+ block = self._get_block(block_name)
|
|
|
+
|
|
|
+ # the block has been found
|
|
|
+ # check that it doesn't write out of the block
|
|
|
+ offset = address - block.starting_address
|
|
|
+
|
|
|
+ if (offset < 0) or ((offset + size) > block.size):
|
|
|
+ raise OutOfModbusBlockError(
|
|
|
+ "address {0} size {1} is out of block {2}".format(address, size, block_name)
|
|
|
+ )
|
|
|
+
|
|
|
+ # returns the values
|
|
|
+ if size == 1:
|
|
|
+ return tuple([block[offset], ])
|
|
|
+ else:
|
|
|
+ return tuple(block[offset: offset + size])
|
|
|
+
|
|
|
+
|
|
|
+class Databank(object):
|
|
|
+ """A databank is a shared place containing the data of all slaves"""
|
|
|
+
|
|
|
+ def __init__(self, error_on_missing_slave=True):
|
|
|
+ """Constructor"""
|
|
|
+ # the map of slaves by ids
|
|
|
+ self._slaves = {}
|
|
|
+ # protect access to the map of slaves
|
|
|
+ self._lock = threading.RLock()
|
|
|
+ self.error_on_missing_slave = error_on_missing_slave
|
|
|
+
|
|
|
+ def add_slave(self, slave_id, unsigned=True, memory=None):
|
|
|
+ """Add a new slave with the given id"""
|
|
|
+ with self._lock:
|
|
|
+ if (slave_id <= 0) or (slave_id > 255):
|
|
|
+ raise Exception("Invalid slave id {0}".format(slave_id))
|
|
|
+ if slave_id not in self._slaves:
|
|
|
+ self._slaves[slave_id] = Slave(slave_id, unsigned, memory)
|
|
|
+ return self._slaves[slave_id]
|
|
|
+ else:
|
|
|
+ raise DuplicatedKeyError("Slave {0} already exists".format(slave_id))
|
|
|
+
|
|
|
+ def get_slave(self, slave_id):
|
|
|
+ """Get the slave with the given id"""
|
|
|
+ with self._lock:
|
|
|
+ if slave_id in self._slaves:
|
|
|
+ return self._slaves[slave_id]
|
|
|
+ else:
|
|
|
+ raise MissingKeyError("Slave {0} doesn't exist".format(slave_id))
|
|
|
+
|
|
|
+ def remove_slave(self, slave_id):
|
|
|
+ """Remove the slave with the given id"""
|
|
|
+ with self._lock:
|
|
|
+ if slave_id in self._slaves:
|
|
|
+ self._slaves.pop(slave_id)
|
|
|
+ else:
|
|
|
+ raise MissingKeyError("Slave {0} already exists".format(slave_id))
|
|
|
+
|
|
|
+ def remove_all_slaves(self):
|
|
|
+ """clean the list of slaves"""
|
|
|
+ with self._lock:
|
|
|
+ self._slaves.clear()
|
|
|
+
|
|
|
+ def handle_request(self, query, request):
|
|
|
+ """
|
|
|
+ when a request is received, handle it and returns the response pdu
|
|
|
+ """
|
|
|
+ request_pdu = ""
|
|
|
+ try:
|
|
|
+ # extract the pdu and the slave id
|
|
|
+ (slave_id, request_pdu) = query.parse_request(request)
|
|
|
+
|
|
|
+ # get the slave and let him executes the action
|
|
|
+ if slave_id == 0:
|
|
|
+ # broadcast
|
|
|
+ for key in self._slaves:
|
|
|
+ self._slaves[key].handle_request(request_pdu, broadcast=True)
|
|
|
+ return
|
|
|
+ else:
|
|
|
+ try:
|
|
|
+ slave = self.get_slave(slave_id)
|
|
|
+ except MissingKeyError:
|
|
|
+ if self.error_on_missing_slave:
|
|
|
+ raise
|
|
|
+ else:
|
|
|
+ return ""
|
|
|
+
|
|
|
+ response_pdu = slave.handle_request(request_pdu)
|
|
|
+ # make the full response
|
|
|
+ response = query.build_response(response_pdu)
|
|
|
+ return response
|
|
|
+ except ModbusInvalidRequestError as excpt:
|
|
|
+ # Request is invalid, do not send any response
|
|
|
+ LOGGER.error("invalid request: " + str(excpt))
|
|
|
+ return ""
|
|
|
+ except MissingKeyError as excpt:
|
|
|
+ # No slave with this ID in server, do not send any response
|
|
|
+ LOGGER.error("handle request failed: " + str(excpt))
|
|
|
+ return ""
|
|
|
+ except Exception as excpt:
|
|
|
+ call_hooks("modbus.Databank.on_error", (self, excpt, request_pdu))
|
|
|
+ LOGGER.error("handle request failed: " + str(excpt))
|
|
|
+
|
|
|
+ # If the request was not handled correctly, return a server error response
|
|
|
+ func_code = 1
|
|
|
+ if len(request_pdu) > 0:
|
|
|
+ (func_code, ) = struct.unpack(">B", request_pdu[0:1])
|
|
|
+
|
|
|
+ return struct.pack(">BB", func_code + 0x80, defines.SLAVE_DEVICE_FAILURE)
|
|
|
+
|
|
|
+
|
|
|
+class Server(object):
|
|
|
+ """
|
|
|
+ This class owns several slaves and defines an interface
|
|
|
+ to be implemented for a TCP or RTU server
|
|
|
+ """
|
|
|
+
|
|
|
+ def __init__(self, databank=None):
|
|
|
+ """Constructor"""
|
|
|
+ # never use a mutable type as default argument
|
|
|
+ self._databank = databank if databank else Databank()
|
|
|
+ self._verbose = False
|
|
|
+ self._thread = None
|
|
|
+ self._go = None
|
|
|
+ self._make_thread()
|
|
|
+
|
|
|
+ def _do_init(self):
|
|
|
+ """executed before the server starts: to be overridden"""
|
|
|
+ pass
|
|
|
+
|
|
|
+ def _do_exit(self):
|
|
|
+ """executed after the server stops: to be overridden"""
|
|
|
+ pass
|
|
|
+
|
|
|
+ def _do_run(self):
|
|
|
+ """main function of the server: to be overridden"""
|
|
|
+ pass
|
|
|
+
|
|
|
+ def _make_thread(self):
|
|
|
+ """create the main thread of the server"""
|
|
|
+ self._thread = threading.Thread(target=Server._run_server, args=(self,))
|
|
|
+ self._go = threading.Event()
|
|
|
+
|
|
|
+ def set_verbose(self, verbose):
|
|
|
+ """if verbose is true the sent and received packets will be logged"""
|
|
|
+ self._verbose = verbose
|
|
|
+
|
|
|
+ def get_db(self):
|
|
|
+ """returns the databank"""
|
|
|
+ return self._databank
|
|
|
+
|
|
|
+ def add_slave(self, slave_id, unsigned=True, memory=None):
|
|
|
+ """add slave to the server"""
|
|
|
+ return self._databank.add_slave(slave_id, unsigned, memory)
|
|
|
+
|
|
|
+ def get_slave(self, slave_id):
|
|
|
+ """get the slave with the given id"""
|
|
|
+ return self._databank.get_slave(slave_id)
|
|
|
+
|
|
|
+ def remove_slave(self, slave_id):
|
|
|
+ """remove the slave with the given id"""
|
|
|
+ self._databank.remove_slave(slave_id)
|
|
|
+
|
|
|
+ def remove_all_slaves(self):
|
|
|
+ """remove the slave with the given id"""
|
|
|
+ self._databank.remove_all_slaves()
|
|
|
+
|
|
|
+ def _make_query(self):
|
|
|
+ """
|
|
|
+ Returns an instance of a Query subclass implementing
|
|
|
+ the MAC layer protocol
|
|
|
+ """
|
|
|
+ raise NotImplementedError()
|
|
|
+
|
|
|
+ def start(self):
|
|
|
+ """Start the server. It will handle request"""
|
|
|
+ self._go.set()
|
|
|
+ self._thread.start()
|
|
|
+
|
|
|
+ def stop(self):
|
|
|
+ """stop the server. It doesn't handle request anymore"""
|
|
|
+ if self._thread.is_alive():
|
|
|
+ self._go.clear()
|
|
|
+ self._thread.join()
|
|
|
+
|
|
|
+ def _run_server(self):
|
|
|
+ """main function of the main thread"""
|
|
|
+ try:
|
|
|
+ self._do_init()
|
|
|
+ while self._go.isSet():
|
|
|
+ self._do_run()
|
|
|
+ LOGGER.debug("%s has stopped", self.__class__)
|
|
|
+ self._do_exit()
|
|
|
+ except Exception as excpt:
|
|
|
+ LOGGER.error("server error: %s", str(excpt))
|
|
|
+ call_hooks("modbus.Server.on_exception", (self, excpt))
|
|
|
+ # make possible to rerun in future
|
|
|
+ self._make_thread()
|
|
|
+
|
|
|
+ def _handle(self, request):
|
|
|
+ """handle a received sentence"""
|
|
|
+
|
|
|
+ if self._verbose:
|
|
|
+ LOGGER.debug(get_log_buffer("-->", request))
|
|
|
+
|
|
|
+ # gets a query for analyzing the request
|
|
|
+ query = self._make_query()
|
|
|
+
|
|
|
+ retval = call_hooks("modbus.Server.before_handle_request", (self, request))
|
|
|
+ if retval:
|
|
|
+ request = retval
|
|
|
+
|
|
|
+ response = self._databank.handle_request(query, request)
|
|
|
+ retval = call_hooks("modbus.Server.after_handle_request", (self, response))
|
|
|
+ if retval:
|
|
|
+ response = retval
|
|
|
+
|
|
|
+ if response and self._verbose:
|
|
|
+ LOGGER.debug(get_log_buffer("<--", response))
|
|
|
+ return response
|