0
votes

I am trying to create Modbus RTU client which will read data from serial port using pymodbus library. I am able to connect to Modbus RTU running on COM2 in Windows10 & able to read data of different types like Int32, Float, etc.

Issue:

After some time I've disconnected my device & checked the status of ModbusClient. My client is connected to COM2 port & trying to read from the device which is not available & call for read_holding_registers blocked.

Environment:

Python: 3.6.5
pymodbus: 2.1.0
Windows: 10 64bit

According to me, it should throw an error something like below

[Failure instance: Traceback (failure with no frames): <class 'twisted.internet.error.ConnectionRefusedError'>: Connection was refused by other side: 10061: No connection could be made because the target machine actively refused it.

OR

[Failure instance: Traceback (failure with no frames): <class 'pymodbus.exceptions.ConnectionException'>: Modbus Error: [Connection] Client is not connected

The above error I'm getting when disconnected from Modbus TCP device. But, there is no action done in case of Modbus RTU.

Below code handle connection lost & failed events:

from pymodbus.client.common import ModbusClientMixin
from twisted.internet import reactor, protocol

class CustomModbusClientFactory(protocol.ClientFactory, ModbusClientMixin):

    def buildProtocol(self, addr=None):
        modbusClientProtocol = CustomModbusClientProtocol()
        modbusClientProtocol.factory = self
        return modbusClientProtocol

    def clientConnectionLost(self, connector, reason):
        logger.critical("Connection lost with device running on {0}:{1}.".format(modbusTcpDeviceIP, modbusTcpDevicePort))
        logger.critical("Root Cause : {0}".format(reason))
        connector.connect()

    def clientConnectionFailed(self, connector, reason):
        logger.critical("Connection failed with device running on {0}:{1}.".format(modbusTcpDeviceIP, modbusTcpDevicePort))
        logger.critical("Root Cause : {0}".format(reason))
        connector.connect()

My complete code is given here : ModbusRTUClient.py

I've make sure availability of Modbus RTU device & raise an alerts if there is any issue with communication with device.

Does anyone have an idea how disconnection & re-connection of Modbus RTU device is handled ?

Any help would be appreciated.

2

2 Answers

0
votes

You are confusing serial communications and TCP/IP ones. They are completely different. When using Modbus RTU, it works over serial lines (mostly it is RS-485 interface in industry, or RS-232 for configuration purposes).

In TCP/IP you have a logical channel (TCP), which is responsible for self-diagnostics and dropping errors when trying to read/write to unconnected endpoint.

With serial lines you just send the data to port (which is done regardless of whether someone on the other side is listening for it) and the only way to understand that your endpoint is down is timeout waiting for a reply.

By the way, there are some point when no reply doesnt mean the device is offline - broadcast messages are an excellent example. For some modbus devices you can broadcast time information on slave 0 and no reply would be granted.

Conclusion: with rtu devices there is no connect/disconnect procedure, you only speak in terms of request/reply.

0
votes

As @grapes said, only request/response format will work in case of RTU device communication. Thus, only option we have is to add timeout which will close the transaction once read timeout occurs.

From the documentation of Twisted, I found method named addTimeout You can check docs from twisted.internet.defer.Deferred.addTimeout(...) which allow to cancel transaction after the amount of time given as timeout.

Once request is timed out, it will pass control to errorHandler of Deferred object. Where you add re-connection logic by calling connectionMade method of ModbusClientProtocol, In my example, it is named as CustomModbusClientProtocol.

My Working Code:

Below is my complete solution for auto-reconnect to Modbus RTU device. Where I am trying to read 10 characters of string data from RTU device.

import logging
from threading import Thread
from time import sleep

from pymodbus.client.async.twisted import ModbusClientProtocol
from pymodbus.constants import Endian
from pymodbus.factory import ClientDecoder
from pymodbus.payload import BinaryPayloadDecoder
from pymodbus.transaction import ModbusRtuFramer
from serial import EIGHTBITS
from serial import PARITY_EVEN
from serial import STOPBITS_ONE
from twisted.internet import protocol
from twisted.internet import serialport, reactor

FORMAT = ('%(asctime)-15s %(threadName)-15s '
      '%(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s')
logging.basicConfig(format=FORMAT)
logger = logging.getLogger()
logger.setLevel(logging.INFO)


def readDevices(modbusRTUDevice):
    deviceIP = modbusRTUDevice["ip"]
    devicePort = modbusRTUDevice["port"]
    logger.info("Connecting to Modbus RTU device at address {0}".format(deviceIP + ":" + str(devicePort)))
    modbusClientFactory = CustomModbusClientFactory()
    modbusClientFactory.address = deviceIP
    modbusClientFactory.modbusDevice = modbusRTUDevice
    SerialModbusClient(modbusClientFactory, devicePort, reactor, baudrate=9600, bytesize=EIGHTBITS,
                   parity=PARITY_EVEN, stopbits=STOPBITS_ONE, xonxoff=0, rtscts=0)
    Thread(target=reactor.run, args=(False,)).start()  # @UndefinedVariable


class SerialModbusClient(serialport.SerialPort):

    def __init__(self, factory, *args, **kwargs):
        serialport.SerialPort.__init__(self, factory.buildProtocol(), *args, **kwargs)


class CustomModbusClientFactory(protocol.ClientFactory):
    modbusDevice = {}

    def buildProtocol(self, addr=None):
        modbusClientProtocol = CustomModbusClientProtocol()
        modbusClientProtocol.factory = self
        modbusClientProtocol.modbusDevice = self.modbusDevice
        return modbusClientProtocol

    def clientConnectionLost(self, connector, reason):
        modbusTcpDeviceIP = self.modbusDevice["ip"]
        modbusTcpDevicePort = self.modbusDevice["port"]
        logger.critical("Connection lost with device running on {0}:{1}.".format(modbusTcpDeviceIP, modbusTcpDevicePort))
        logger.critical("Root Cause : {0}".format(reason))
        connector.connect()

    def clientConnectionFailed(self, connector, reason):
        modbusTcpDeviceIP = self.modbusDevice["ip"]
        modbusTcpDevicePort = self.modbusDevice["port"]
        logger.critical("Connection failed with device running on {0}:{1}.".format(modbusTcpDeviceIP, modbusTcpDevicePort))
        logger.critical("Root Cause : {0}".format(reason))
        connector.connect()


class CustomModbusClientProtocol(ModbusClientProtocol):

    def connectionMade(self):
        framer = ModbusRtuFramer(ClientDecoder(), client=None)
        ModbusClientProtocol.__init__(self, framer)
        ModbusClientProtocol.connectionMade(self)
        deviceIP = self.modbusDevice["ip"]
        devicePort = self.modbusDevice["port"]
        logger.info("Modbus RTU device connected at address {0}".format(deviceIP + ":" + str(devicePort)))
        reactor.callLater(5, self.read)  # @UndefinedVariable

    def read(self):
        deviceIP = self.modbusDevice["ip"]
        devicePort = self.modbusDevice["port"]
        slaveAddress = self.modbusDevice["slaveAddress"]
        deviceReadTimeout = self.modbusDevice["readTimeoutInSeconds"]
        logger.info("Reading holding registers of Modbus RTU device at address {0}...".format(deviceIP + ":" + str(devicePort)))
        deferred = self.read_holding_registers(0, 5, unit=slaveAddress)
        deferred.addCallbacks(self.requestFetched, self.requestNotFetched)
        deferred.addTimeout(deviceReadTimeout, reactor)

    def requestNotFetched(self, error):
        logger.info("Error reading registers of Modbus RTU device : {0}".format(error))
        logger.error("Trying reconnect in next {0} seconds...".format(5))
        reactor.callLater(5, self.connectionMade)  # @UndefinedVariable

    def requestFetched(self, response):
        logger.info("Inside request fetched...")
        decoder = BinaryPayloadDecoder.fromRegisters(response.registers, byteorder=Endian.Big, wordorder=Endian.Big)
        skipBytesCount = 0
        decoder.skip_bytes(skipBytesCount)
        registerValue = decoder.decode_string(10).decode()
        skipBytesCount += 10
        logger.info("Sensor updated to value '{0}'.".format(registerValue))
        reactor.callLater(5, self.read)  # @UndefinedVariable


readDevices({"ip": "127.0.0.1", "port": "COM2", "slaveAddress": 1, "readTimeoutInSeconds": 30})

Output:

2019-02-19 15:40:02,533 MainThread      INFO     TestRTU:26       Connecting to Modbus RTU device at address 127.0.0.1:COM2
2019-02-19 15:40:02,536 MainThread      INFO     TestRTU:73       Modbus RTU device connected at address 127.0.0.1:COM2
2019-02-19 15:40:07,541 Thread-2        INFO     TestRTU:81       Reading holding registers of Modbus RTU device at address 127.0.0.1:COM2...
2019-02-19 15:40:07,662 Thread-2        INFO     TestRTU:92       Inside request fetched...
2019-02-19 15:40:07,662 Thread-2        INFO     TestRTU:98       Sensor updated to value 'abcdefghij'.


2019-02-19 15:40:12,662 Thread-2        INFO     TestRTU:81       Reading holding registers of Modbus RTU device at address 127.0.0.1:COM2...
2019-02-19 15:40:12,773 Thread-2        INFO     TestRTU:92       Inside request fetched...
2019-02-19 15:40:12,773 Thread-2        INFO     TestRTU:98       Sensor updated to value 'abcdefghij'.


2019-02-19 15:40:17,773 Thread-2        INFO     TestRTU:81       Reading holding registers of Modbus RTU device at address 127.0.0.1:COM2...
2019-02-19 15:40:47,773 Thread-2        INFO     TestRTU:87       Error reading registers of Modbus RTU device : [Failure instance: Traceback (failure with no frames): <class 'twisted.internet.defer.CancelledError'>:]
2019-02-19 15:40:47,773 Thread-2        ERROR    TestRTU:88       Trying to reconnect in next 5 seconds...


2019-02-19 15:40:52,780 Thread-2        INFO     TestRTU:73       Modbus RTU device connected at address logger127.0.0.1:COM2
2019-02-19 15:40:57,784 Thread-2        INFO     TestRTU:81       Reading holding registers of Modbus RTU device at address 127.0.0.1:COM2...
2019-02-19 15:40:57,996 Thread-2        INFO     TestRTU:92       Inside request fetched...
2019-02-19 15:40:57,996 Thread-2        INFO     TestRTU:98       Sensor updated to value 'abcdefghij'.

I hope this helps someone in future.