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.