dever 4 years ago
parent
commit
170a7c559c

+ 41 - 0
modbus_tk/__init__.py

@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+ Modbus TestKit: Implementation of Modbus protocol in python
+
+ (C)2009 - Luc Jean - luc.jean@gmail.com
+ (C)2009 - Apidev - http://www.apidev.fr
+
+ This is distributed under GNU LGPL license, see license.txt
+
+ Make possible to write modbus TCP and RTU master and slave for testing purpose
+ Modbus TestKit is different from pymodbus which is another implementation of
+ the modbus stack in python
+
+contributors:
+----------------------------------
+* OrangeTux
+* denisstogl
+* MELabs
+* idahogray
+* riaan.doorduin
+* tor.sjowall
+* smachin1000
+* GadgetSteve 
+* dhoomakethu
+* zerox1212
+* ffreckle
+* Matthew West
+* Vincent Prince
+* kcl93
+* https://github.com/Slasktra
+* travijuu
+
+Please let us know if your name is missing!
+
+"""
+
+VERSION = '1.1.0'
+
+import logging
+LOGGER = logging.getLogger("modbus_tk")

+ 40 - 0
modbus_tk/defines.py

@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+ Modbus TestKit: Implementation of Modbus protocol in python
+
+ (C)2009 - Luc Jean - luc.jean@gmail.com
+ (C)2009 - Apidev - http://www.apidev.fr
+
+ This is distributed under GNU LGPL license, see license.txt
+"""
+
+#modbus exception codes
+ILLEGAL_FUNCTION = 1
+ILLEGAL_DATA_ADDRESS = 2
+ILLEGAL_DATA_VALUE = 3
+SLAVE_DEVICE_FAILURE = 4
+COMMAND_ACKNOWLEDGE = 5
+SLAVE_DEVICE_BUSY = 6
+MEMORY_PARITY_ERROR = 8
+
+#supported modbus functions
+READ_COILS = 1
+READ_DISCRETE_INPUTS = 2
+READ_HOLDING_REGISTERS = 3
+READ_INPUT_REGISTERS = 4
+WRITE_SINGLE_COIL = 5
+WRITE_SINGLE_REGISTER = 6
+READ_EXCEPTION_STATUS = 7
+DIAGNOSTIC = 8
+REPORT_SLAVE_ID = 17
+WRITE_MULTIPLE_COILS = 15
+WRITE_MULTIPLE_REGISTERS = 16
+READ_WRITE_MULTIPLE_REGISTERS = 23
+DEVICE_INFO = 43
+
+#supported block types
+COILS = 1
+DISCRETE_INPUTS = 2
+HOLDING_REGISTERS = 3
+ANALOG_INPUTS = 4

+ 88 - 0
modbus_tk/exceptions.py

@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+"""
+ Modbus TestKit: Implementation of Modbus protocol in python
+
+ (C)2009 - Luc Jean - luc.jean@gmail.com
+ (C)2009 - Apidev - http://www.apidev.fr
+
+ This is distributed under GNU LGPL license, see license.txt
+"""
+
+
+class ModbusError(Exception):
+    """Exception raised when the modbus slave returns an error"""
+
+    def __init__(self, exception_code, value=""):
+        """constructor: set the exception code returned by the slave"""
+        if not value:
+            value = "Modbus Error: Exception code = %d" % (exception_code)
+        Exception.__init__(self, value)
+        self._exception_code = exception_code
+
+    def get_exception_code(self):
+        """return the exception code returned by the slave (see defines)"""
+        return self._exception_code
+
+
+class ModbusFunctionNotSupportedError(Exception):
+    """
+    Exception raised when calling a modbus function not supported by modbus_tk
+    """
+    pass
+
+
+class DuplicatedKeyError(Exception):
+    """
+    Exception raised when trying to add an object with a key that is already
+    used for another object
+    """
+    pass
+
+
+class MissingKeyError(Exception):
+    """
+    Exception raised when trying to get an object with a key that doesn't exist
+    """
+    pass
+
+
+class InvalidModbusBlockError(Exception):
+    """Exception raised when a modbus block is not valid"""
+    pass
+
+
+class InvalidArgumentError(Exception):
+    """
+    Exception raised when one argument of a function doesn't meet
+    what is expected
+    """
+    pass
+
+
+class OverlapModbusBlockError(Exception):
+    """
+    Exception raised when adding modbus block on a memory address
+    range already in use
+    """
+    pass
+
+
+class OutOfModbusBlockError(Exception):
+    """Exception raised when accessing out of a modbus block"""
+    pass
+
+
+class ModbusInvalidResponseError(Exception):
+    """
+    Exception raised when the response sent by the slave doesn't fit
+    with the expected format
+    """
+    pass
+
+
+class ModbusInvalidRequestError(Exception):
+    """
+    Exception raised when the request by the master doesn't fit
+    with the expected format
+    """
+    pass

+ 106 - 0
modbus_tk/hooks.py

@@ -0,0 +1,106 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+ Modbus TestKit: Implementation of Modbus protocol in python
+
+ (C)2009 - Luc Jean - luc.jean@gmail.com
+ (C)2009 - Apidev - http://www.apidev.fr
+
+ This is distributed under GNU LGPL license, see license.txt
+"""
+
+from __future__ import with_statement
+import threading
+
+_LOCK = threading.RLock()
+_HOOKS = {}
+
+
+def install_hook(name, fct):
+    """
+    Install one of the following hook
+
+    modbus_rtu.RtuMaster.before_open((master,))
+    modbus_rtu.RtuMaster.after_close((master,)
+    modbus_rtu.RtuMaster.before_send((master, request)) returns modified request or None
+    modbus_rtu.RtuMaster.after_recv((master, response)) returns modified response or None
+
+    modbus_rtu.RtuServer.before_close((server, ))
+    modbus_rtu.RtuServer.after_close((server, ))
+    modbus_rtu.RtuServer.before_open((server, ))
+    modbus_rtu.RtuServer.after_open(((server, ))
+    modbus_rtu.RtuServer.after_read((server, request)) returns modified request or None
+    modbus_rtu.RtuServer.before_write((server, response))  returns modified response or None
+    modbus_rtu.RtuServer.after_write((server, response))
+    modbus_rtu.RtuServer.on_error((server, excpt))
+
+    modbus_tcp.TcpMaster.before_connect((master, ))
+    modbus_tcp.TcpMaster.after_connect((master, ))
+    modbus_tcp.TcpMaster.before_close((master, ))
+    modbus_tcp.TcpMaster.after_close((master, ))
+    modbus_tcp.TcpMaster.before_send((master, request))
+    modbus_tcp.TcpServer.after_send((master, request))
+    modbus_tcp.TcpMaster.after_recv((master, response))
+
+
+    modbus_tcp.TcpServer.on_connect((server, client, address))
+    modbus_tcp.TcpServer.on_disconnect((server, sock))
+    modbus_tcp.TcpServer.after_recv((server, sock, request)) returns modified request or None
+    modbus_tcp.TcpServer.before_send((server, sock, response)) returns modified response or None
+    modbus_tcp.TcpServer.on_error((server, sock, excpt))
+
+    modbus_rtu_over_tcp.RtuOverTcpMaster.after_recv((master, response))
+
+    modbus.Master.before_send((master, request)) returns modified request or None
+    modbus.Master.after_send((master))
+    modbus.Master.after_recv((master, response)) returns modified response or None
+
+    modbus.Slave.handle_request((slave, request_pdu)) returns modified response or None
+    modbus.Slave.handle_write_multiple_coils_request((slave, request_pdu))
+    modbus.Slave.handle_write_multiple_registers_request((slave, request_pdu)) returns modified response or None
+    modbus.Slave.handle_write_single_register_request((slave, request_pdu)) returns modified response or None
+    modbus.Slave.handle_write_single_coil_request((slave, request_pdu)) returns modified response or None
+    modbus.Slave.handle_read_input_registers_request((slave, request_pdu)) returns modified response or None
+    modbus.Slave.handle_read_holding_registers_request((slave, request_pdu)) returns modified response or None
+    modbus.Slave.handle_read_discrete_inputs_request((slave, request_pdu)) returns modified response or None
+    modbus.Slave.handle_read_coils_request((slave, request_pdu)) returns modified response or None
+
+    modbus.Slave.on_handle_broadcast((slave, response_pdu)) returns modified response or None
+    modbus.Slave.on_exception((slave, function_code, excpt))
+
+
+    modbus.Databank.on_error((db, excpt, request_pdu))
+
+    modbus.ModbusBlock.setitem((self, slice, value))
+
+    modbus.Server.before_handle_request((server, request)) returns modified request or None
+    modbus.Server.after_handle_request((server, response)) returns modified response or None
+    """
+    with _LOCK:
+        try:
+            _HOOKS[name].append(fct)
+        except KeyError:
+            _HOOKS[name] = [fct]
+
+
+def uninstall_hook(name, fct=None):
+    """remove the function from the hooks"""
+    with _LOCK:
+        if fct:
+            _HOOKS[name].remove(fct)
+        else:
+            del _HOOKS[name][:]
+
+
+def call_hooks(name, args):
+    """call the function associated with the hook and pass the given args"""
+    with _LOCK:
+        try:
+            for fct in _HOOKS[name]:
+                retval = fct(args)
+                if retval is not None:
+                    return retval
+        except KeyError:
+            pass
+        return None
+

+ 959 - 0
modbus_tk/modbus.py

@@ -0,0 +1,959 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+ Modbus TestKit: Implementation of Modbus protocol in python
+
+ (C)2009 - Luc Jean - luc.jean@gmail.com
+ (C)2009 - Apidev - http://www.apidev.fr
+
+ This is distributed under GNU LGPL license, see license.txt
+
+ History:
+ 2010/01/08 - RD: Update master.execute(..) to calculate lengths automatically based on requested command
+"""
+
+from __future__ import with_statement
+
+import struct
+import threading
+
+from modbus_tk import LOGGER
+from modbus_tk import defines
+from modbus_tk.exceptions import(
+    ModbusError, ModbusFunctionNotSupportedError, DuplicatedKeyError, MissingKeyError, InvalidModbusBlockError,
+    InvalidArgumentError, OverlapModbusBlockError, OutOfModbusBlockError, ModbusInvalidResponseError,
+    ModbusInvalidRequestError
+)
+from modbus_tk.hooks import call_hooks
+from modbus_tk.utils import threadsafe_function, get_log_buffer
+
+# modbus_tk is using the python logging mechanism
+# you can define this logger in your app in order to see its prints logs
+
+
+class Query(object):
+    """
+    Interface to be implemented in subclass for every specific modbus MAC layer
+    """
+
+    def __init__(self):
+        """Constructor"""
+        pass
+
+    def build_request(self, pdu, slave):
+        """
+        Get the modbus application protocol request pdu and slave id
+        Encapsulate with MAC layer information
+        Returns a string
+        """
+        raise NotImplementedError()
+
+    def parse_response(self, response):
+        """
+        Get the full response and extract the modbus application protocol
+        response pdu
+        Returns a string
+        """
+        raise NotImplementedError()
+
+    def parse_request(self, request):
+        """
+        Get the full request and extract the modbus application protocol
+        request pdu
+        Returns a string and the slave id
+        """
+        raise NotImplementedError()
+
+    def build_response(self, response_pdu):
+        """
+        Get the modbus application protocol response pdu and encapsulate with
+        MAC layer information
+        Returns a string
+        """
+        raise NotImplementedError()
+
+
+class Master(object):
+    """
+    This class implements the Modbus Application protocol for a master
+    To be subclassed with a class implementing the MAC layer
+    """
+
+    def __init__(self, timeout_in_sec, hooks=None):
+        """Constructor: can define a timeout"""
+        self._timeout = timeout_in_sec
+        self._verbose = False
+        self._is_opened = False
+
+    def __del__(self):
+        """Destructor: close the connection"""
+        self.close()
+
+    def set_verbose(self, verbose):
+        """print some more log prints for debug purpose"""
+        self._verbose = verbose
+
+    def open(self):
+        """open the communication with the slave"""
+        if not self._is_opened:
+            self._do_open()
+            self._is_opened = True
+
+    def close(self):
+        """close the communication with the slave"""
+        if self._is_opened:
+            ret = self._do_close()
+            if ret:
+                self._is_opened = False
+
+    def _do_open(self):
+        """Open the MAC layer"""
+        raise NotImplementedError()
+
+    def _do_close(self):
+        """Close the MAC layer"""
+        raise NotImplementedError()
+
+    def _send(self, buf):
+        """Send data to a slave on the MAC layer"""
+        raise NotImplementedError()
+
+    def _recv(self, expected_length):
+        """
+        Receive data from a slave on the MAC layer
+        if expected_length is >=0 then consider that the response is done when this
+        number of bytes is received
+        """
+        raise NotImplementedError()
+
+    def _make_query(self):
+        """
+        Returns an instance of a Query subclass implementing
+        the MAC layer protocol
+        """
+        raise NotImplementedError()
+
+    @threadsafe_function
+    def execute(
+        self, slave, function_code, starting_address, quantity_of_x=0, output_value=0, data_format="", expected_length=-1):
+        """
+        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
+        """
+
+        pdu = ""
+        is_read_function = False
+        nb_of_digits = 0
+
+        # 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.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, defines.READ_WRITE_MULTIPLE_REGISTERS,
+                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 lenght was specified and calculated length can be used:
+                # slave + func + bytcodeLen + bytecode x 2 + crc1 + crc2
+                expected_length = 2 * quantity_of_x + 5
+        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))
+                        )
+                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)
+                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)
+                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):
+    """This class represents the values for a range of addresses"""
+
+    def __init__(self, starting_address, size, name=''):
+        """
+        Contructor: defines the address range and creates the array of values
+        """
+        self.starting_address = starting_address
+        self._data = [0] * size
+        self.size = len(self._data)
+
+    def is_in(self, starting_address, size):
+        """
+        Returns true if a block with the given address and size
+        would overlap this block
+        """
+        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):
+    """
+    This class define a modbus slave which is in charge of making the action
+    asked by a modbus query
+    """
+
+    def __init__(self, slave_id, unsigned=True, memory=None):
+        """Constructor"""
+        self._id = slave_id
+
+        # treat every value written to/read from register as an unsigned value
+        self.unsigned = unsigned
+
+        # the map registring all blocks of the slave
+        self._blocks = {}
+        # a shortcut to find blocks per type
+        if memory is None:
+            self._memory = {
+                defines.COILS: [],
+                defines.DISCRETE_INPUTS: [],
+                defines.HOLDING_REGISTERS: [],
+                defines.ANALOG_INPUTS: [],
+            }
+        else:
+            self._memory = memory
+        # a lock for mutual access to the _blocks and _memory maps
+        self._data_lock = threading.RLock()
+        # map modbus function code to a function:
+        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.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,
+        }
+
+    def _get_block_and_offset(self, block_type, address, length):
+        """returns the block and offset corresponding to the given address"""
+        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):
+        """read the value of coils and discrete inputs"""
+        (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 _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.isAlive():
+            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.info("%s has stopped", self.__class__)
+            self._do_exit()
+        except Exception as excpt:
+            LOGGER.error("server error: %s", str(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

+ 307 - 0
modbus_tk/modbus_rtu.py

@@ -0,0 +1,307 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+ Modbus TestKit: Implementation of Modbus protocol in python
+
+ (C)2009 - Luc Jean - luc.jean@gmail.com
+ (C)2009 - Apidev - http://www.apidev.fr
+
+ This is distributed under GNU LGPL license, see license.txt
+
+"""
+
+import struct
+import time
+
+from modbus_tk import LOGGER
+from modbus_tk.modbus import (
+    Databank, Query, Master, Server,
+    InvalidArgumentError, ModbusInvalidResponseError, ModbusInvalidRequestError
+)
+from modbus_tk.hooks import call_hooks
+from modbus_tk import utils
+
+
+class RtuQuery(Query):
+    """Subclass of a Query. Adds the Modbus RTU specific part of the protocol"""
+
+    def __init__(self):
+        """Constructor"""
+        super(RtuQuery, self).__init__()
+        self._request_address = 0
+        self._response_address = 0
+
+    def build_request(self, pdu, slave):
+        """Add the Modbus RTU part to the request"""
+        self._request_address = slave
+        if (self._request_address < 0) or (self._request_address > 255):
+            raise InvalidArgumentError("Invalid address {0}".format(self._request_address))
+        data = struct.pack(">B", self._request_address) + pdu
+        crc = struct.pack(">H", utils.calculate_crc(data))
+        return data + crc
+
+    def parse_response(self, response):
+        """Extract the pdu from the Modbus RTU response"""
+        if len(response) < 3:
+            raise ModbusInvalidResponseError("Response length is invalid {0}".format(len(response)))
+
+        (self._response_address, ) = struct.unpack(">B", response[0:1])
+
+        if self._request_address != self._response_address:
+            raise ModbusInvalidResponseError(
+                "Response address {0} is different from request address {1}".format(
+                    self._response_address, self._request_address
+                )
+            )
+
+        (crc, ) = struct.unpack(">H", response[-2:])
+
+        if crc != utils.calculate_crc(response[:-2]):
+            raise ModbusInvalidResponseError("Invalid CRC in response")
+
+        return response[1:-2]
+
+    def parse_request(self, request):
+        """Extract the pdu from the Modbus RTU request"""
+        if len(request) < 3:
+            raise ModbusInvalidRequestError("Request length is invalid {0}".format(len(request)))
+
+        (self._request_address, ) = struct.unpack(">B", request[0:1])
+
+        (crc, ) = struct.unpack(">H", request[-2:])
+        if crc != utils.calculate_crc(request[:-2]):
+            raise ModbusInvalidRequestError("Invalid CRC in request")
+
+        return self._request_address, request[1:-2]
+
+    def build_response(self, response_pdu):
+        """Build the response"""
+        self._response_address = self._request_address
+        data = struct.pack(">B", self._response_address) + response_pdu
+        crc = struct.pack(">H", utils.calculate_crc(data))
+        return data + crc
+
+
+class RtuMaster(Master):
+    """Subclass of Master. Implements the Modbus RTU MAC layer"""
+
+    def __init__(self, serial, interchar_multiplier=1.5, interframe_multiplier=3.5, t0=None):
+        """Constructor. Pass the pyserial.Serial object"""
+        self._serial = serial
+        self.use_sw_timeout = False
+        LOGGER.info("RtuMaster %s is %s", self._serial.name, "opened" if self._serial.is_open else "closed")
+        super(RtuMaster, self).__init__(self._serial.timeout)
+
+        if t0:
+            self._t0 = t0
+        else:
+            self._t0 = utils.calculate_rtu_inter_char(self._serial.baudrate)
+        self._serial.inter_byte_timeout = interchar_multiplier * self._t0
+        self.set_timeout(interframe_multiplier * self._t0)
+
+        # For some RS-485 adapters, the sent data(echo data) appears before modbus response.
+        # So read  echo data and discard it.  By yush0602@gmail.com
+        self.handle_local_echo = False
+
+    def _do_open(self):
+        """Open the given serial port if not already opened"""
+        if not self._serial.is_open:
+            call_hooks("modbus_rtu.RtuMaster.before_open", (self, ))
+            self._serial.open()
+
+    def _do_close(self):
+        """Close the serial port if still opened"""
+        if self._serial.is_open:
+            self._serial.close()
+            call_hooks("modbus_rtu.RtuMaster.after_close", (self, ))
+            return True
+
+    def set_timeout(self, timeout_in_sec, use_sw_timeout=False):
+        """Change the timeout value"""
+        Master.set_timeout(self, timeout_in_sec)
+        self._serial.timeout = timeout_in_sec
+        # Use software based timeout in case the timeout functionality provided by the serial port is unreliable
+        self.use_sw_timeout = use_sw_timeout
+
+    def _send(self, request):
+        """Send request to the slave"""
+        retval = call_hooks("modbus_rtu.RtuMaster.before_send", (self, request))
+        if retval is not None:
+            request = retval
+
+        self._serial.reset_input_buffer()
+        self._serial.reset_output_buffer()
+
+        self._serial.write(request)
+        self._serial.flush()
+
+        # Read the echo data, and discard it
+        if self.handle_local_echo:
+            self._serial.read(len(request))
+
+    def _recv(self, expected_length=-1):
+        """Receive the response from the slave"""
+        response = utils.to_data("")
+        start_time = time.time() if self.use_sw_timeout else 0
+        while True:
+            read_bytes = self._serial.read(expected_length if expected_length > 0 else 1)
+            if self.use_sw_timeout:
+                read_duration = time.time() - start_time
+            else:
+                read_duration = 0
+            if (not read_bytes) or (read_duration > self._serial.timeout):
+                break
+            response += read_bytes
+            if expected_length >= 0 and len(response) >= expected_length:
+                # if the expected number of byte is received consider that the response is done
+                # improve performance by avoiding end-of-response detection by timeout
+                break
+
+        retval = call_hooks("modbus_rtu.RtuMaster.after_recv", (self, response))
+        if retval is not None:
+            return retval
+        return response
+
+    def _make_query(self):
+        """Returns an instance of a Query subclass implementing the modbus RTU protocol"""
+        return RtuQuery()
+
+
+class RtuServer(Server):
+    """This class implements a simple and mono-threaded modbus rtu server"""
+    _timeout = 0
+
+    def __init__(self, serial, databank=None, error_on_missing_slave=True, **kwargs):
+        """
+        Constructor: initializes the server settings
+        serial: a pyserial object
+        databank: the data to access
+        interframe_multiplier: 3.5 by default
+        interchar_multiplier: 1.5 by default
+        """
+        interframe_multiplier = kwargs.pop('interframe_multiplier', 3.5)
+        interchar_multiplier = kwargs.pop('interchar_multiplier', 1.5)
+
+        databank = databank if databank else Databank(error_on_missing_slave=error_on_missing_slave)
+        super(RtuServer, self).__init__(databank)
+
+        self._serial = serial
+        LOGGER.info("RtuServer %s is %s", self._serial.name, "opened" if self._serial.is_open else "closed")
+
+        self._t0 = utils.calculate_rtu_inter_char(self._serial.baudrate)
+        self._serial.inter_byte_timeout = interchar_multiplier * self._t0
+        self.set_timeout(interframe_multiplier * self._t0)
+
+        self._block_on_first_byte = False
+
+    def close(self):
+        """close the serial communication"""
+        if self._serial.is_open:
+            call_hooks("modbus_rtu.RtuServer.before_close", (self, ))
+            self._serial.close()
+            call_hooks("modbus_rtu.RtuServer.after_close", (self, ))
+
+    def set_timeout(self, timeout):
+        self._timeout = timeout
+        self._serial.timeout = timeout
+
+    def get_timeout(self):
+        return self._timeout
+
+    def __del__(self):
+        """Destructor"""
+        self.close()
+
+    def _make_query(self):
+        """Returns an instance of a Query subclass implementing the modbus RTU protocol"""
+        return RtuQuery()
+
+    def start(self):
+        """Allow the server thread to block on first byte"""
+        self._block_on_first_byte = True
+        super(RtuServer, self).start()
+
+    def stop(self):
+        """Force the server thread to exit"""
+        # Prevent blocking on first byte in server thread.
+        # Without the _block_on_first_byte following problem could happen:
+        #   1. Current blocking read(1) is cancelled
+        #   2. Server thread resumes and start next read(1)
+        #   3. RtuServer clears go event and waits for thread to finish
+        #   4. Server thread finishes only when a byte is received
+        # Thanks to _block_on_first_byte, if server thread does start new read
+        # it will timeout as it won't be blocking.
+        self._block_on_first_byte = False
+        if self._serial.is_open:
+            # cancel any pending read from server thread, it most likely is
+            # blocking read(1) call
+            self._serial.cancel_read()
+        super(RtuServer, self).stop()
+
+    def _do_init(self):
+        """initialize the serial connection"""
+        if not self._serial.is_open:
+            call_hooks("modbus_rtu.RtuServer.before_open", (self, ))
+            self._serial.open()
+            call_hooks("modbus_rtu.RtuServer.after_open", (self, ))
+
+    def _do_exit(self):
+        """close the serial connection"""
+        self.close()
+
+    def _do_run(self):
+        """main function of the server"""
+        try:
+            # check the status of every socket
+            request = utils.to_data('')
+            if self._block_on_first_byte:
+                # do a blocking read for first byte
+                self._serial.timeout = None
+                try:
+                    read_bytes = self._serial.read(1)
+                    request += read_bytes
+                except Exception as e:
+                    self._serial.close()
+                    self._serial.open()
+                self._serial.timeout = self._timeout
+
+            # Read rest of the request
+            while True:
+                try:
+                    read_bytes = self._serial.read(128)
+                    if not read_bytes:
+                        break
+                except Exception as e:
+                    self._serial.close()
+                    self._serial.open()
+                    break
+                request += read_bytes
+
+            # parse the request
+            if request:
+                retval = call_hooks("modbus_rtu.RtuServer.after_read", (self, request))
+                if retval is not None:
+                    request = retval
+
+                response = self._handle(request)
+
+                # send back the response
+                retval = call_hooks("modbus_rtu.RtuServer.before_write", (self, response))
+                if retval is not None:
+                    response = retval
+
+                if response:
+                    if self._serial.in_waiting > 0:
+                        # Most likely master timed out on this request and started a new one
+                        # for which we already received atleast 1 byte
+                        LOGGER.warning("Not sending response because there is new request pending")
+                    else:
+                        self._serial.write(response)
+                        self._serial.flush()
+                        time.sleep(self.get_timeout())
+
+                call_hooks("modbus_rtu.RtuServer.after_write", (self, response))
+
+        except Exception as excpt:
+            LOGGER.error("Error while handling request, Exception occurred: %s", excpt)
+            call_hooks("modbus_rtu.RtuServer.on_error", (self, excpt))

+ 38 - 0
modbus_tk/modbus_rtu_over_tcp.py

@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+ Modbus TestKit: Implementation of Modbus protocol in python
+
+ (C)2009 - Luc Jean - luc.jean@gmail.com
+ (C)2009 - Apidev - http://www.apidev.fr
+
+ This is distributed under GNU LGPL license, see license.txt
+"""
+
+from modbus_tk.hooks import call_hooks
+from modbus_tk.modbus_rtu import RtuQuery
+from modbus_tk.modbus_tcp import TcpMaster
+from modbus_tk.utils import to_data
+
+
+class RtuOverTcpMaster(TcpMaster):
+    """Subclass of TcpMaster. Implements the Modbus RTU over TCP MAC layer"""
+
+    def _recv(self, expected_length=-1):
+        """Receive the response from the slave"""
+        response = to_data('')
+        length = 255
+        while len(response) < length:
+            rcv_byte = self._sock.recv(1)
+            if rcv_byte:
+                response += rcv_byte
+            if expected_length >= 0 and len(response) >= expected_length:
+                break
+        retval = call_hooks("modbus_rtu_over_tcp.RtuOverTcpMaster.after_recv", (self, response))
+        if retval is not None:
+            return retval
+        return response
+
+    def _make_query(self):
+        """Returns an instance of a Query subclass implementing the modbus RTU protocol"""
+        return RtuQuery()

+ 360 - 0
modbus_tk/modbus_tcp.py

@@ -0,0 +1,360 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+ Modbus TestKit: Implementation of Modbus protocol in python
+
+ (C)2009 - Luc Jean - luc.jean@gmail.com
+ (C)2009 - Apidev - http://www.apidev.fr
+
+ This is distributed under GNU LGPL license, see license.txt
+"""
+
+import socket
+import select
+import struct
+
+from modbus_tk import LOGGER
+from modbus_tk.hooks import call_hooks
+from modbus_tk.modbus import (
+    Databank, Master, Query, Server,
+    InvalidArgumentError, ModbusInvalidResponseError, ModbusInvalidRequestError
+)
+from modbus_tk.utils import threadsafe_function, flush_socket, to_data
+
+
+#-------------------------------------------------------------------------------
+class ModbusInvalidMbapError(Exception):
+    """Exception raised when the modbus TCP header doesn't correspond to what is expected"""
+
+    def __init__(self, value):
+        Exception.__init__(self, value)
+
+
+#-------------------------------------------------------------------------------
+class TcpMbap(object):
+    """Defines the information added by the Modbus TCP layer"""
+
+    def __init__(self):
+        """Constructor: initializes with 0"""
+        self.transaction_id = 0
+        self.protocol_id = 0
+        self.length = 0
+        self.unit_id = 0
+
+    def clone(self, mbap):
+        """Set the value of each fields from another TcpMbap instance"""
+        self.transaction_id = mbap.transaction_id
+        self.protocol_id = mbap.protocol_id
+        self.length = mbap.length
+        self.unit_id = mbap.unit_id
+
+    def _check_ids(self, request_mbap):
+        """
+        Check that the ids in the request and the response are similar.
+        if not returns a string describing the error
+        """
+        error_str = ""
+
+        if request_mbap.transaction_id != self.transaction_id:
+            error_str += "Invalid transaction id: request={0} - response={1}. ".format(
+                request_mbap.transaction_id, self.transaction_id)
+
+        if request_mbap.protocol_id != self.protocol_id:
+            error_str += "Invalid protocol id: request={0} - response={1}. ".format(
+                request_mbap.protocol_id, self.protocol_id
+            )
+
+        if request_mbap.unit_id != self.unit_id:
+            error_str += "Invalid unit id: request={0} - response={1}. ".format(request_mbap.unit_id, self.unit_id)
+
+        return error_str
+
+    def check_length(self, pdu_length):
+        """Check the length field is valid. If not raise an exception"""
+        following_bytes_length = pdu_length+1
+        if self.length != following_bytes_length:
+            return "Response length is {0} while receiving {1} bytes. ".format(self.length, following_bytes_length)
+        return ""
+
+    def check_response(self, request_mbap, response_pdu_length):
+        """Check that the MBAP of the response is valid. If not raise an exception"""
+        error_str = self._check_ids(request_mbap)
+        error_str += self.check_length(response_pdu_length)
+        if len(error_str) > 0:
+            raise ModbusInvalidMbapError(error_str)
+
+    def pack(self):
+        """convert the TCP mbap into a string"""
+        return struct.pack(">HHHB", self.transaction_id, self.protocol_id, self.length, self.unit_id)
+
+    def unpack(self, value):
+        """extract the TCP mbap from a string"""
+        (self.transaction_id, self.protocol_id, self.length, self.unit_id) = struct.unpack(">HHHB", value)
+
+
+class TcpQuery(Query):
+    """Subclass of a Query. Adds the Modbus TCP specific part of the protocol"""
+
+    #static variable for giving a unique id to each query
+    _last_transaction_id = 0
+
+    def __init__(self):
+        """Constructor"""
+        super(TcpQuery, self).__init__()
+        self._request_mbap = TcpMbap()
+        self._response_mbap = TcpMbap()
+
+    @threadsafe_function
+    def _get_transaction_id(self):
+        """returns an identifier for the query"""
+        if TcpQuery._last_transaction_id < 0xffff:
+            TcpQuery._last_transaction_id += 1
+        else:
+            TcpQuery._last_transaction_id = 0
+        return TcpQuery._last_transaction_id
+
+    def build_request(self, pdu, slave):
+        """Add the Modbus TCP part to the request"""
+        if (slave < 0) or (slave > 255):
+            raise InvalidArgumentError("{0} Invalid value for slave id".format(slave))
+        self._request_mbap.length = len(pdu) + 1
+        self._request_mbap.transaction_id = self._get_transaction_id()
+        self._request_mbap.unit_id = slave
+        mbap = self._request_mbap.pack()
+        return mbap + pdu
+
+    def parse_response(self, response):
+        """Extract the pdu from the Modbus TCP response"""
+        if len(response) > 6:
+            mbap, pdu = response[:7], response[7:]
+            self._response_mbap.unpack(mbap)
+            self._response_mbap.check_response(self._request_mbap, len(pdu))
+            return pdu
+        else:
+            raise ModbusInvalidResponseError("Response length is only {0} bytes. ".format(len(response)))
+
+    def parse_request(self, request):
+        """Extract the pdu from a modbus request"""
+        if len(request) > 6:
+            mbap, pdu = request[:7], request[7:]
+            self._request_mbap.unpack(mbap)
+            error_str = self._request_mbap.check_length(len(pdu))
+            if len(error_str) > 0:
+                raise ModbusInvalidMbapError(error_str)
+            return self._request_mbap.unit_id, pdu
+        else:
+            raise ModbusInvalidRequestError("Request length is only {0} bytes. ".format(len(request)))
+
+    def build_response(self, response_pdu):
+        """Build the response"""
+        self._response_mbap.clone(self._request_mbap)
+        self._response_mbap.length = len(response_pdu) + 1
+        return self._response_mbap.pack() + response_pdu
+
+
+class TcpMaster(Master):
+    """Subclass of Master. Implements the Modbus TCP MAC layer"""
+
+    def __init__(self, host="127.0.0.1", port=502, timeout_in_sec=5.0):
+        """Constructor. Set the communication settings"""
+        super(TcpMaster, self).__init__(timeout_in_sec)
+        self._host = host
+        self._port = port
+        self._sock = None
+
+    def _do_open(self):
+        """Connect to the Modbus slave"""
+        if self._sock:
+            self._sock.close()
+        self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        self.set_timeout(self.get_timeout())
+        self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        call_hooks("modbus_tcp.TcpMaster.before_connect", (self, ))
+        self._sock.connect((self._host, self._port))
+        call_hooks("modbus_tcp.TcpMaster.after_connect", (self, ))
+
+    def _do_close(self):
+        """Close the connection with the Modbus Slave"""
+        if self._sock:
+            call_hooks("modbus_tcp.TcpMaster.before_close", (self, ))
+            self._sock.close()
+            call_hooks("modbus_tcp.TcpMaster.after_close", (self, ))
+            self._sock = None
+            return True
+
+    def set_timeout(self, timeout_in_sec):
+        """Change the timeout value"""
+        super(TcpMaster, self).set_timeout(timeout_in_sec)
+        if self._sock:
+            self._sock.setblocking(timeout_in_sec > 0)
+            if timeout_in_sec:
+                self._sock.settimeout(timeout_in_sec)
+
+    def _send(self, request):
+        """Send request to the slave"""
+        retval = call_hooks("modbus_tcp.TcpMaster.before_send", (self, request))
+        if retval is not None:
+            request = retval
+        try:
+            flush_socket(self._sock, 3)
+        except Exception as msg:
+            #if we can't flush the socket successfully: a disconnection may happened
+            #try to reconnect
+            LOGGER.error('Error while flushing the socket: {0}'.format(msg))
+            self._do_open()
+        self._sock.send(request)
+
+    def _recv(self, expected_length=-1):
+        """
+        Receive the response from the slave
+        Do not take expected_length into account because the length of the response is
+        written in the mbap. Used for RTU only
+        """
+        response = to_data('')
+        length = 255
+        while len(response) < length:
+            rcv_byte = self._sock.recv(1)
+            if rcv_byte:
+                response += rcv_byte
+                if len(response) == 6:
+                    to_be_recv_length = struct.unpack(">HHH", response)[2]
+                    length = to_be_recv_length + 6
+            else:
+                break
+        retval = call_hooks("modbus_tcp.TcpMaster.after_recv", (self, response))
+        if retval is not None:
+            return retval
+        return response
+
+    def _make_query(self):
+        """Returns an instance of a Query subclass implementing the modbus TCP protocol"""
+        return TcpQuery()
+
+
+class TcpServer(Server):
+    """
+    This class implements a simple and mono-threaded modbus tcp server
+    !! Change in 0.5.0: By default the TcpServer is not bound to a specific address
+    for example: You must set address to 'loaclhost', if youjust want to accept local connections
+    """
+
+    def __init__(self, port=502, address='', timeout_in_sec=1, databank=None, error_on_missing_slave=True):
+        """Constructor: initializes the server settings"""
+        databank = databank if databank else Databank(error_on_missing_slave=error_on_missing_slave)
+        super(TcpServer, self).__init__(databank)
+        self._sock = None
+        self._sa = (address, port)
+        self._timeout_in_sec = timeout_in_sec
+        self._sockets = []
+
+    def _make_query(self):
+        """Returns an instance of a Query subclass implementing the modbus TCP protocol"""
+        return TcpQuery()
+
+    def _get_request_length(self, mbap):
+        """Parse the mbap and returns the number of bytes to be read"""
+        if len(mbap) < 6:
+            raise ModbusInvalidRequestError("The mbap is only %d bytes long", len(mbap))
+        length = struct.unpack(">HHH", mbap[:6])[2]
+        return length
+
+    def _do_init(self):
+        """initialize server"""
+        self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        if self._timeout_in_sec:
+            self._sock.settimeout(self._timeout_in_sec)
+        self._sock.setblocking(0)
+        self._sock.bind(self._sa)
+        self._sock.listen(10)
+        self._sockets.append(self._sock)
+
+    def _do_exit(self):
+        """clean the server tasks"""
+        #close the sockets
+        for sock in self._sockets:
+            try:
+                sock.close()
+                self._sockets.remove(sock)
+            except Exception as msg:
+                LOGGER.warning("Error while closing socket, Exception occurred: %s", msg)
+        self._sock.close()
+        self._sock = None
+
+    def _do_run(self):
+        """called in a almost-for-ever loop by the server"""
+        # check the status of every socket
+        inputready = select.select(self._sockets, [], [], 1.0)[0]
+
+        # handle data on each a socket
+        for sock in inputready:
+            try:
+                if sock == self._sock:
+                    # handle the server socket
+                    client, address = self._sock.accept()
+                    client.setblocking(0)
+                    LOGGER.info("%s is connected with socket %d...", str(address), client.fileno())
+                    self._sockets.append(client)
+                    call_hooks("modbus_tcp.TcpServer.on_connect", (self, client, address))
+                else:
+                    if len(sock.recv(1, socket.MSG_PEEK)) == 0:
+                        # socket is disconnected
+                        LOGGER.info("%d is disconnected" % (sock.fileno()))
+                        call_hooks("modbus_tcp.TcpServer.on_disconnect", (self, sock))
+                        sock.close()
+                        self._sockets.remove(sock)
+                        break
+
+                    # handle all other sockets
+                    sock.settimeout(1.0)
+                    request = to_data("")
+                    is_ok = True
+
+                    # read the 7 bytes of the mbap
+                    while (len(request) < 7) and is_ok:
+                        new_byte = sock.recv(1)
+                        if len(new_byte) == 0:
+                            is_ok = False
+                        else:
+                            request += new_byte
+
+                    retval = call_hooks("modbus_tcp.TcpServer.after_recv", (self, sock, request))
+                    if retval is not None:
+                        request = retval
+
+                    if is_ok:
+                        # read the rest of the request
+                        length = self._get_request_length(request)
+                        while (len(request) < (length + 6)) and is_ok:
+                            new_byte = sock.recv(1)
+                            if len(new_byte) == 0:
+                                is_ok = False
+                            else:
+                                request += new_byte
+
+                    if is_ok:
+                        response = ""
+                        # parse the request
+                        try:
+                            response = self._handle(request)
+                        except Exception as msg:
+                            LOGGER.error("Error while handling a request, Exception occurred: %s", msg)
+
+                        # send back the response
+                        if response:
+                            try:
+                                retval = call_hooks("modbus_tcp.TcpServer.before_send", (self, sock, response))
+                                if retval is not None:
+                                    response = retval
+                                sock.send(response)
+                                call_hooks("modbus_tcp.TcpServer.after_send", (self, sock, response))
+                            except Exception as msg:
+                                is_ok = False
+                                LOGGER.error(
+                                    "Error while sending on socket %d, Exception occurred: %s", sock.fileno(), msg
+                                )
+            except Exception as excpt:
+                LOGGER.warning("Error while processing data on socket %d: %s", sock.fileno(), excpt)
+                call_hooks("modbus_tcp.TcpServer.on_error", (self, sock, excpt))
+                sock.close()
+                self._sockets.remove(sock)

+ 382 - 0
modbus_tk/simulator.py

@@ -0,0 +1,382 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+ Modbus TestKit: Implementation of Modbus protocol in python
+
+ (C)2009 - Luc Jean - luc.jean@gmail.com
+ (C)2009 - Apidev - http://www.apidev.fr
+
+ This is distributed under GNU LGPL license, see license.txt
+
+ The modbus_tk simulator is a console application which is running a server with TCP and RTU communication
+ It is possible to interact with the server from the command line or from a RPC (Remote Process Call)
+"""
+from __future__ import print_function
+
+import ctypes
+import os
+import sys
+import select
+import serial
+import threading
+import time
+
+import modbus_tk
+from modbus_tk import hooks
+from modbus_tk import modbus
+from modbus_tk import modbus_tcp
+from modbus_tk import modbus_rtu
+
+if modbus_tk.utils.PY2:
+    import Queue as queue
+    import SocketServer
+else:
+    import queue
+    import socketserver as SocketServer
+
+
+# add logging capability
+LOGGER = modbus_tk.utils.create_logger(name="console", record_format="%(message)s")
+
+# The communication between the server and the user interfaces (console or rpc) are done through queues
+
+# command received from the interfaces
+INPUT_QUEUE = queue.Queue()
+
+# response to be sent back by the interfaces
+OUTPUT_QUEUE = queue.Queue()
+
+
+class CompositeServer(modbus.Server):
+    """make possible to have several servers sharing the same databank"""
+
+    def __init__(self, list_of_server_classes, list_of_server_args, databank=None):
+        """Constructor"""
+        super(CompositeServer, self).__init__(databank)
+        self._servers = [
+            the_class(*the_args, **{"databank": self.get_db()})
+            for the_class, the_args in zip(list_of_server_classes, list_of_server_args)
+            if issubclass(the_class, modbus.Server)
+        ]
+
+    def set_verbose(self, verbose):
+        """if verbose is true the sent and received packets will be logged"""
+        for srv in self._servers:
+            srv.set_verbose(verbose)
+
+    def _make_thread(self):
+        """should initialize the main thread of the server. You don't need it here"""
+        pass
+
+    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"""
+        for srv in self._servers:
+            srv.start()
+
+    def stop(self):
+        """stop the server. It doesn't handle request anymore"""
+        for srv in self._servers:
+            srv.stop()
+
+
+class RpcHandler(SocketServer.BaseRequestHandler):
+    """An instance of this class is created every time an RPC call is received by the server"""
+
+    def handle(self):
+        """This function is called automatically by the SocketServer"""
+        # self.request is the TCP socket connected to the client
+        # read the incoming command
+        request = self.request.recv(1024).strip()
+        # write to the queue waiting to be processed by the server
+        INPUT_QUEUE.put(request)
+        # wait for the server answer in the output queue
+        response = OUTPUT_QUEUE.get(timeout=5.0)
+        # send back the answer
+        self.request.send(response)
+
+
+class RpcInterface(threading.Thread):
+    """Manage RPC call over TCP/IP thanks to the SocketServer module"""
+
+    def __init__(self):
+        """Constructor"""
+        super(RpcInterface, self).__init__()
+        self.rpc_server = SocketServer.TCPServer(("", 2711), RpcHandler)
+
+    def run(self):
+        """run the server and wait that it returns"""
+        self.rpc_server.serve_forever(0.5)
+
+    def close(self):
+        """force the socket server to exit"""
+        try:
+            self.rpc_server.shutdown()
+            self.join(1.0)
+        except Exception:
+            LOGGER.warning("An error occurred while closing RPC interface")
+
+
+class ConsoleInterface(threading.Thread):
+    """Manage user actions from the console"""
+
+    def __init__(self):
+        """constructor: initialize communication with the console"""
+        super(ConsoleInterface, self).__init__()
+        self.inq = INPUT_QUEUE
+        self.outq = OUTPUT_QUEUE
+
+        if os.name == "nt":
+            ctypes.windll.Kernel32.GetStdHandle.restype = ctypes.c_ulong
+            self.console_handle = ctypes.windll.Kernel32.GetStdHandle(ctypes.c_ulong(0xfffffff5))
+            ctypes.windll.Kernel32.WaitForSingleObject.restype = ctypes.c_ulong
+
+        elif os.name == "posix":
+            # select already imported
+            pass
+
+        else:
+            raise Exception("%s platform is not supported yet" % os.name)
+
+        self._go = threading.Event()
+        self._go.set()
+
+    def _check_console_input(self):
+        """test if there is something to read on the console"""
+
+        if os.name == "nt":
+            if 0 == ctypes.windll.Kernel32.WaitForSingleObject(self.console_handle, 500):
+                return True
+
+        elif os.name == "posix":
+            (inputready, abcd, efgh) = select.select([sys.stdin], [], [], 0.5)
+            if len(inputready) > 0:
+                return True
+
+        else:
+            raise Exception("%s platform is not supported yet" % os.name)
+
+        return False
+
+    def run(self):
+        """read from the console, transfer to the server and write the answer"""
+        while self._go.isSet(): #while app is running
+            if self._check_console_input(): #if something to read on the console
+                cmd = sys.stdin.readline() #read it
+                self.inq.put(cmd) #dispatch it tpo the server
+                response = self.outq.get(timeout=2.0) #wait for an answer
+                sys.stdout.write(response) #write the answer on the console
+
+    def close(self):
+        """terminates the thread"""
+        self._go.clear()
+        self.join(1.0)
+
+
+class Simulator(object):
+    """The main class of the app in charge of running everything"""
+
+    def __init__(self, server=None):
+        """Constructor"""
+        if server is None:
+            self.server = CompositeServer([modbus_rtu.RtuServer, modbus_tcp.TcpServer], [(serial.Serial(0),), ()])
+        else:
+            self.server = server
+        self.rpc = RpcInterface()
+        self.console = ConsoleInterface()
+        self.inq, self.outq = INPUT_QUEUE, OUTPUT_QUEUE
+        self._hooks_fct = {}
+
+        self.cmds = {
+            "add_slave": self._do_add_slave,
+            "has_slave": self._do_has_slave,
+            "remove_slave": self._do_remove_slave,
+            "remove_all_slaves": self._do_remove_all_slaves,
+            "add_block": self._do_add_block,
+            "remove_block": self._do_remove_block,
+            "remove_all_blocks": self._do_remove_all_blocks,
+            "set_values": self._do_set_values,
+            "get_values": self._do_get_values,
+            "install_hook": self._do_install_hook,
+            "uninstall_hook": self._do_uninstall_hook,
+            "set_verbose": self._do_set_verbose,
+        }
+
+    def add_command(self, name, fct):
+        """add a custom command"""
+        self.cmds[name] = fct
+
+    def start(self):
+        """run the servers"""
+        self.server.start()
+        self.console.start()
+        self.rpc.start()
+
+        LOGGER.info("modbus_tk.simulator is running...")
+
+        self._handle()
+
+    def declare_hook(self, fct_name, fct):
+        """declare a hook function by its name. It must be installed by an install hook command"""
+        self._hooks_fct[fct_name] = fct
+
+    def _tuple_to_str(self, the_tuple):
+        """convert a tuple to a string"""
+        ret = ""
+        for item in the_tuple:
+            ret += (" " + str(item))
+        return ret[1:]
+
+    def _do_add_slave(self, args):
+        """execute the add_slave command"""
+        slave_id = int(args[1])
+        self.server.add_slave(slave_id)
+        return "{0}".format(slave_id)
+
+    def _do_has_slave(self, args):
+        """execute the has_slave command"""
+        slave_id = int(args[1])
+        try:
+            self.server.get_slave(slave_id)
+        except Exception:
+            return "0"
+        return "1"
+
+    def _do_remove_slave(self, args):
+        """execute the remove_slave command"""
+        slave_id = int(args[1])
+        self.server.remove_slave(slave_id)
+        return ""
+
+    def _do_remove_all_slaves(self, args):
+        """execute the remove_slave command"""
+        self.server.remove_all_slaves()
+        return ""
+
+    def _do_add_block(self, args):
+        """execute the add_block command"""
+        slave_id = int(args[1])
+        name = args[2]
+        block_type = int(args[3])
+        starting_address = int(args[4])
+        length = int(args[5])
+        slave = self.server.get_slave(slave_id)
+        slave.add_block(name, block_type, starting_address, length)
+        return name
+
+    def _do_remove_block(self, args):
+        """execute the remove_block command"""
+        slave_id = int(args[1])
+        name = args[2]
+        slave = self.server.get_slave(slave_id)
+        slave.remove_block(name)
+
+    def _do_remove_all_blocks(self, args):
+        """execute the remove_all_blocks command"""
+        slave_id = int(args[1])
+        slave = self.server.get_slave(slave_id)
+        slave.remove_all_blocks()
+
+    def _do_set_values(self, args):
+        """execute the set_values command"""
+        slave_id = int(args[1])
+        name = args[2]
+        address = int(args[3])
+        values = []
+        for val in args[4:]:
+            values.append(int(val))
+        slave = self.server.get_slave(slave_id)
+        slave.set_values(name, address, values)
+        values = slave.get_values(name, address, len(values))
+        return self._tuple_to_str(values)
+
+    def _do_get_values(self, args):
+        """execute the get_values command"""
+        slave_id = int(args[1])
+        name = args[2]
+        address = int(args[3])
+        length = int(args[4])
+        slave = self.server.get_slave(slave_id)
+        values = slave.get_values(name, address, length)
+        return self._tuple_to_str(values)
+
+    def _do_install_hook(self, args):
+        """install a function as a hook"""
+        hook_name = args[1]
+        fct_name = args[2]
+        hooks.install_hook(hook_name, self._hooks_fct[fct_name])
+
+    def _do_uninstall_hook(self, args):
+        """
+        uninstall a function as a hook.
+        If no function is given, uninstall all functions
+        """
+        hook_name = args[1]
+        try:
+            hooks.uninstall_hook(hook_name)
+        except KeyError as exception:
+            LOGGER.error(str(exception))
+
+    def _do_set_verbose(self, args):
+        """change the verbosity of the server"""
+        verbose = int(args[1])
+        self.server.set_verbose(verbose)
+        return "%d" % verbose
+
+    def _handle(self):
+        """almost-for-ever loop in charge of listening for command and executing it"""
+        while True:
+            cmd = self.inq.get()
+            args = cmd.strip('\r\n').split(' ')
+            if cmd.find('quit') == 0:
+                self.outq.put('bye-bye\r\n')
+                break
+            elif args[0] in self.cmds:
+                try:
+                    answer = self.cmds[args[0]](args)
+                    self.outq.put("%s done: %s\r\n" % (args[0], answer))
+                except Exception as msg:
+                    self.outq.put("%s error: %s\r\n" % (args[0], msg))
+            else:
+                self.outq.put("error: unknown command %s\r\n" % (args[0]))
+
+    def close(self):
+        """close every server"""
+        self.console.close()
+        self.rpc.close()
+        self.server.stop()
+
+
+def print_me(args):
+    """hook function example"""
+    request = args[1]
+    print("print_me: len = ", len(request))
+
+
+def run_simulator():
+    """run simulator"""
+    simulator = Simulator()
+
+    try:
+        LOGGER.info("'quit' for closing the server")
+
+        simulator.declare_hook("print_me", print_me)
+        simulator.start()
+
+    except Exception as exception:
+        print(exception)
+
+    finally:
+        simulator.close()
+        LOGGER.info("modbus_tk.simulator has stopped!")
+        # In python 2.5, the SocketServer shutdown is not working Ok
+        # The 2 lines below are an ugly temporary workaround
+        time.sleep(1.0)
+        sys.exit()
+
+
+if __name__ == "__main__":
+    run_simulator()

+ 127 - 0
modbus_tk/simulator_rpc_client.py

@@ -0,0 +1,127 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+ Modbus TestKit: Implementation of Modbus protocol in python
+
+ (C)2009 - Luc Jean - luc.jean@gmail.com
+ (C)2009 - Apidev - http://www.apidev.fr
+
+ This is distributed under GNU LGPL license, see license.txt
+"""
+from __future__ import print_function
+
+import socket
+import modbus_tk.defines
+
+
+class SimulatorRpcClient(object):
+    """Make possible to send command to the modbus_tk.Simulator thanks to Remote Process Call"""
+
+    def __init__(self, host="127.0.0.1", port=2711, timeout=0.5):
+        """Constructor"""
+        self.host = host
+        self.port = port
+        self.timeout = timeout
+
+    def _rpc_call(self, query):
+        """send a rpc call and return the result"""
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        sock.settimeout(self.timeout)
+        sock.connect((self.host, self.port))
+        sock.send(query)
+        response = sock.recv(1024)
+        sock.close()
+        return self._response_to_values(response.strip("\r\n"), query.split(" ")[0])
+
+    def _response_to_values(self, response, command):
+        """extract the return value from the response"""
+        prefix = command + " done: "
+        if response.find(prefix) == 0:
+            return response[len(prefix):]
+        else:
+            raise Exception(response)
+
+    def add_slave(self, slave_id):
+        """add a new slave with the given id"""
+        query = "add_slave %d" % (slave_id)
+        return self._rpc_call(query)
+
+    def remove_slave(self, slave_id):
+        """add a new slave with the given id"""
+        query = "remove_slave %d" % (slave_id)
+        return self._rpc_call(query)
+
+    def remove_all_slaves(self):
+        """add a new slave with the given id"""
+        query = "remove_all_slaves"
+        self._rpc_call(query)
+
+    def has_slave(self, slave_id):
+        """add a new slave with the given id"""
+        query = "has_slave %d" % (slave_id)
+        if "1" == self._rpc_call(query):
+            return True
+        return False
+
+    def add_block(self, slave_id, block_name, block_type, starting_address, length):
+        """add a new modbus block into the slave"""
+        query = "add_block %d %s %d %d %d" % (slave_id, block_name, block_type, starting_address, length)
+        return self._rpc_call(query)
+
+    def remove_block(self, slave_id, block_name):
+        """remove the modbus block with the given name and slave"""
+        query = "remove_block %d %s" % (slave_id, block_name)
+        self._rpc_call(query)
+
+    def remove_all_blocks(self, slave_id):
+        """remove the modbus block with the given name and slave"""
+        query = "remove_all_blocks %d" % (slave_id)
+        self._rpc_call(query)
+
+    def set_values(self, slave_id, block_name, address, values):
+        """set the values of registers"""
+        query = "set_values %d %s %d" % (slave_id, block_name, address)
+        for val in values:
+            query += (" " + str(val))
+        return self._rpc_call(query)
+
+    def get_values(self, slave_id, block_name, address, length):
+        """get the values of some registers"""
+        query = "get_values %d %s %d %d" % (slave_id, block_name, address, length)
+        ret_values = self._rpc_call(query)
+        return tuple([int(val) for val in ret_values.split(' ')])
+
+    def install_hook(self, hook_name, fct_name):
+        """add a hook"""
+        query = "install_hook %s %s" % (hook_name, fct_name)
+        self._rpc_call(query)
+
+    def uninstall_hook(self, hook_name, fct_name=""):
+        """remove a hook"""
+        query = "uninstall_hook %s %s" % (hook_name, fct_name)
+        self._rpc_call(query)
+
+
+if __name__ == "__main__":
+    modbus_simu = SimulatorRpcClient()
+    modbus_simu.remove_all_slaves()
+    print(modbus_simu.add_slave(12))
+    print(modbus_simu.add_block(12, "toto", modbus_tk.defines.COILS, 0, 100))
+    print(modbus_simu.set_values(12, "toto", 0, [5, 8, 7, 6, 41]))
+    print(modbus_simu.get_values(12, "toto", 0, 5))
+    print(modbus_simu.set_values(12, "toto", 2, [9]))
+    print(modbus_simu.get_values(12, "toto", 0, 5))
+    print(modbus_simu.has_slave(12))
+    print(modbus_simu.add_block(12, "titi", modbus_tk.defines.COILS, 100, 100))
+    print(modbus_simu.remove_block(12, "titi"))
+    print(modbus_simu.add_slave(25))
+    print(modbus_simu.has_slave(25))
+    print(modbus_simu.add_slave(28))
+    modbus_simu.remove_slave(25)
+    print(modbus_simu.has_slave(25))
+    print(modbus_simu.has_slave(28))
+    modbus_simu.remove_all_blocks(12)
+    modbus_simu.remove_all_slaves()
+    print(modbus_simu.has_slave(28))
+    print(modbus_simu.has_slave(12))
+    modbus_simu.install_hook("modbus.Server.before_handle_request", "print_me")

+ 237 - 0
modbus_tk/utils.py

@@ -0,0 +1,237 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+ Modbus TestKit: Implementation of Modbus protocol in python
+
+ (C)2009 - Luc Jean - luc.jean@gmail.com
+ (C)2009 - Apidev - http://www.apidev.fr
+
+ This is distributed under GNU LGPL license, see license.txt
+"""
+from __future__ import print_function
+
+import sys
+import threading
+import logging
+import socket
+import select
+from modbus_tk import LOGGER
+
+PY2 = sys.version_info[0] == 2
+PY3 = sys.version_info[0] == 3
+
+
+def threadsafe_function(fcn):
+    """decorator making sure that the decorated function is thread safe"""
+    lock = threading.RLock()
+
+    def new(*args, **kwargs):
+        """Lock and call the decorated function
+
+           Unless kwargs['threadsafe'] == False
+        """
+        threadsafe = kwargs.pop('threadsafe', True)
+        if threadsafe:
+            lock.acquire()
+        try:
+            ret = fcn(*args, **kwargs)
+        except Exception as excpt:
+            raise excpt
+        finally:
+            if threadsafe:
+                lock.release()
+        return ret
+    return new
+
+
+def flush_socket(socks, lim=0):
+    """remove the data present on the socket"""
+    input_socks = [socks]
+    cnt = 0
+    while True:
+        i_socks = select.select(input_socks, input_socks, input_socks, 0.0)[0]
+        if len(i_socks) == 0:
+            break
+        for sock in i_socks:
+            sock.recv(1024)
+        if lim > 0:
+            cnt += 1
+            if cnt >= lim:
+                #avoid infinite loop due to loss of connection
+                raise Exception("flush_socket: maximum number of iterations reached")
+
+
+def get_log_buffer(prefix, buff):
+    """Format binary data into a string for debug purpose"""
+    log = prefix
+    for i in buff:
+        log += str(ord(i) if PY2 else i) + "-"
+    return log[:-1]
+
+
+class ConsoleHandler(logging.Handler):
+    """This class is a logger handler. It prints on the console"""
+
+    def __init__(self):
+        """Constructor"""
+        logging.Handler.__init__(self)
+
+    def emit(self, record):
+        """format and print the record on the console"""
+        print(self.format(record))
+
+
+class LogitHandler(logging.Handler):
+    """This class is a logger handler. It send to a udp socket"""
+
+    def __init__(self, dest):
+        """Constructor"""
+        logging.Handler.__init__(self)
+        self._dest = dest
+        self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+
+    def emit(self, record):
+        """format and send the record over udp"""
+        data = self.format(record) + "\r\n"
+        if PY3:
+            data = to_data(data)
+        self._sock.sendto(data, self._dest)
+
+
+class DummyHandler(logging.Handler):
+    """This class is a logger handler. It doesn't do anything"""
+
+    def __init__(self):
+        """Constructor"""
+        super(DummyHandler, self).__init__()
+
+    def emit(self, record):
+        """do nothing with the given record"""
+        pass
+
+
+def create_logger(name="dummy", level=logging.DEBUG, record_format=None):
+    """Create a logger according to the given settings"""
+    if record_format is None:
+        record_format = "%(asctime)s\t%(levelname)s\t%(module)s.%(funcName)s\t%(threadName)s\t%(message)s"
+
+    logger = logging.getLogger("modbus_tk")
+    logger.setLevel(level)
+    formatter = logging.Formatter(record_format)
+    if name == "udp":
+        log_handler = LogitHandler(("127.0.0.1", 1975))
+    elif name == "console":
+        log_handler = ConsoleHandler()
+    elif name == "dummy":
+        log_handler = DummyHandler()
+    else:
+        raise Exception("Unknown handler %s" % name)
+    log_handler.setFormatter(formatter)
+    logger.addHandler(log_handler)
+    return logger
+
+
+def swap_bytes(word_val):
+    """swap lsb and msb of a word"""
+    msb = (word_val >> 8) & 0xFF
+    lsb = word_val & 0xFF
+    return (lsb << 8) + msb
+
+
+def calculate_crc(data):
+    """Calculate the CRC16 of a datagram"""
+    CRC16table = (
+        0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
+        0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
+        0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
+        0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
+        0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
+        0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
+        0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
+        0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
+        0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
+        0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
+        0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
+        0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
+        0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
+        0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
+        0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
+        0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
+        0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
+        0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
+        0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
+        0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
+        0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
+        0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
+        0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
+        0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
+        0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
+        0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
+        0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
+        0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
+        0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
+        0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
+        0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
+        0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040
+    )
+    crc = 0xFFFF
+    if PY2:
+        for c in data:
+            crc = (crc >> 8) ^ CRC16table[(ord(c) ^ crc) & 0xFF]
+    else:
+        for c in data:
+            crc = (crc >> 8) ^ CRC16table[((c) ^ crc) & 0xFF]
+    return swap_bytes(crc)
+
+
+def calculate_rtu_inter_char(baudrate):
+    """calculates the interchar delay from the baudrate"""
+    if baudrate <= 19200:
+        return 11.0 / baudrate
+    else:
+        return 0.0005
+
+
+class WorkerThread(object):
+    """
+    A thread which is running an almost-ever loop
+    It can be stopped by calling the stop function
+    """
+    def __init__(self, main_fct, args=(), init_fct=None, exit_fct=None):
+        """Constructor"""
+        self._fcts = [init_fct, main_fct, exit_fct]
+        self._args = args
+        self._thread = threading.Thread(target=WorkerThread._run, args=(self,))
+        self._go = threading.Event()
+
+    def start(self):
+        """Start the thread"""
+        self._go.set()
+        self._thread.start()
+
+    def stop(self):
+        """stop the thread"""
+        if self._thread.isAlive():
+            self._go.clear()
+            self._thread.join()
+
+    def _run(self):
+        """main function of the thread execute _main_fct until stop is called"""
+        #pylint: disable=broad-except
+        try:
+            if self._fcts[0]:
+                self._fcts[0](*self._args)
+            while self._go.isSet():
+                self._fcts[1](*self._args)
+        except Exception as excpt:
+            LOGGER.error("error: %s", str(excpt))
+        finally:
+            if self._fcts[2]:
+                self._fcts[2](*self._args)
+
+
+def to_data(string_data):
+    if PY2:
+        return string_data
+    else:
+        return bytearray(string_data, 'ascii')

+ 3 - 3
service/modbus/handle.py

@@ -10,7 +10,7 @@ class Handle(object):
 
 	# 设置数据
 	def set(self, master, server):
-		#master.set_timeout(6.0)
+		master.set_timeout(600)
 		server['server_time'] = int(server['server_time'])
 		while(True):
 			self.run(master, server)
@@ -36,8 +36,8 @@ class Handle(object):
 
 	# 发送数据
 	def send(self, master, server, type_info, info, value):
-		#master.set_timeout(6.0)
-		#master.set_verbose(True)
+		master.set_timeout(600)
+		master.set_verbose(True)
 		code = Demeter.service('common').one('setting_modbus_code', id=info['code_id'])
 		if not code:
 			return ''