0

I am trying to create Modbus serial/RTU client which will read data from serial port using Pymodbus library.

Python: 3.6
Pymodbus: 2.1.0
Platform: Linux/Windows

My sample code base is given below:

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)
    Thread(target=reactor.run, args=(False,)).start() 

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

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

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

class CustomModbusClientProtocol(ModbusClientProtocol):
    def connectionMade(self):
        framer = ModbusRtuFramer.__init__(self, ClientDecoder(), client=None)
        ModbusClientProtocol.__init__(self, framer, baudrate=9600, parity='E', bytesize=8, stopbits=1, timeout=0.2, retryOnEmpty=True, retries=3)
        ModbusClientProtocol.connectionMade(self)
        deviceIP = self.modbusDevice["ip"]
        devicePort = self.modbusDevice["port"]
        logger.info("Modbus RTU device connected at address {0}".format(deviceIP + ":" + str(devicePort)))
        self.read()

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

    def requestNotFetched(self, error):
        logger.info("Error reading registers of Modbus RTU device : {0}".format(error))
        sleep(0.5)

    def requestFetched(self, response):
        logger.info("Inside request fetched...")
        #Do some other stuff here
        reactor.callLater(0, self.read) 

After debugging, self of read() method says CustomModbusClientProtocol: Null Transport. After this, I press F8 & thread goes in blocking state & callbacks of deferred never called.

Output:

INFO:__main__: (2019-01-30 15:42:53; Test.py:200 Connecting to Modbus RTU device at address 127.0.0.1:/dev/ttyUSB0)
INFO:__main__: (2019-01-30 15:42:53; Test.py:70 Modbus RTU device connected at address 127.0.0.1:/dev/ttyUSB0)
INFO:__main__: (2019-01-30 15:46:18; Test.py:87 Reading holding registers of Modbus RTU device at address 127.0.0.1:/dev/ttyUSB0...)

According to Serial + Async + RTU: Callback is never fired #160, the issue of calling callback of deferred is fixed. But, in my case it is still persist.

I've tried this code in both, Raspberry PI & Windows, getting same response in both the cases.

Can't understand why this is happening. Do I need to add something in my code or should I did something wrong while connecting to Modbus device on serial port ?

Any help would be appreciated.

OO7
  • 2,785
  • 1
  • 21
  • 33

2 Answers2

0

EDIT

Updated code and logs


The problem is you are calling self.read() directly in connectionMade of CustomModbusClientProtocol class, before the reactor is actually started. You will have to wait for the reactor to start and schedule the read at a later time with reactor.callLater(<time-to-wait-in-seconds>, self.read). The modified CustomModbusClientProtocol code would look something like this

from twisted.internet import serialport, reactor
from twisted.internet import protocol
from pymodbus.factory import ClientDecoder
from pymodbus.client.async.twisted import ModbusClientProtocol

from pymodbus.transaction import ModbusRtuFramer
from threading import Thread
from time import sleep
import logging
FORMAT = ('%(asctime)-15s %(threadName)-15s '
          '%(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s')
logging.basicConfig(format=FORMAT)
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)


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)
    Thread(target=reactor.run, args=(False,)).start()

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

class CustomModbusClientProtocol(ModbusClientProtocol):
    def connectionMade(self):
        framer = ModbusRtuFramer(ClientDecoder(), client=None)
        ModbusClientProtocol.__init__(self, framer, baudrate=9600, parity='E', bytesize=8, stopbits=1, timeout=0.2, retryOnEmpty=True, retries=3)
        ModbusClientProtocol.connectionMade(self)
        deviceIP = self.modbusDevice["ip"]
        devicePort = self.modbusDevice["port"]
        logger.info("Modbus RTU device connected at address logger{0}".format(deviceIP + ":" + str(devicePort)))
        reactor.callLater(5, self.read)

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

    def requestNotFetched(self, error):
        logger.info("Error reading registers of Modbus RTU device : {0}".format(error))
        sleep(0.5)

    def requestFetched(self, response):
        logger.info("Inside request fetched...")
        #Do some other stuff here
        reactor.callLater(0, self.read)


readDevices({"ip": "127.0.0.1", "port": "/dev/ptyp0", "slaveAddress": 1})

Logs:

$ python scratch_118.py
2019-01-31 12:34:56,734 MainThread      INFO     scratch_118    :21       Connecting to Modbus RTU device at address 127.0.0.1:/dev/ptyp0
2019-01-31 12:34:56,735 MainThread      DEBUG    __init__       :80       Client connected to modbus server
2019-01-31 12:34:56,735 MainThread      INFO     scratch_118    :48       Modbus RTU device connected at address logger127.0.0.1:/dev/ptyp0
2019-01-31 12:35:01,737 Thread-1        INFO     scratch_118    :55       Reading holding registers of Modbus RTU device at address 127.0.0.1:/dev/ptyp0...
2019-01-31 12:35:01,738 Thread-1        DEBUG    __init__       :112      send: 0x1 0x3 0x0 0x0 0x0 0xa 0xc5 0xcd
2019-01-31 12:35:01,738 Thread-1        DEBUG    transaction    :418      Adding transaction 1
2019-01-31 12:35:02,196 Thread-1        DEBUG    rtu_framer     :175      Getting Frame - 0x3 0x14 0x0 0x2d 0x0 0x2c 0x0 0x2e 0x0 0x2d 0x0 0x2d 0x0 0x2e 0x0 0x29 0x0 0x2d 0x0 0x2d 0x0 0x2c
2019-01-31 12:35:02,197 Thread-1        DEBUG    factory        :246      Factory Response[ReadHoldingRegistersResponse: 3]
2019-01-31 12:35:02,197 Thread-1        DEBUG    rtu_framer     :110      Frame advanced, resetting header!!
2019-01-31 12:35:02,197 Thread-1        INFO     scratch_118    :64       Inside request fetched...
2019-01-31 12:35:02,197 Thread-1        INFO     scratch_118    :55       Reading holding registers of Modbus RTU device at address 127.0.0.1:/dev/ptyp0...
2019-01-31 12:35:02,198 Thread-1        DEBUG    __init__       :112      send: 0x1 0x3 0x0 0x0 0x0 0xa 0xc5 0xcd
2019-01-31 12:35:02,198 Thread-1        DEBUG    transaction    :418      Adding transaction 2
2019-01-31 12:35:03,202 Thread-1        DEBUG    rtu_framer     :175      Getting Frame - 0x3 0x14 0x0 0x2d 0x0 0x2c 0x0 0x2e 0x0 0x2d 0x0 0x2d 0x0 0x2e 0x0 0x29 0x0 0x2d 0x0 0x2d 0x0 0x2c
2019-01-31 12:35:03,202 Thread-1        DEBUG    factory        :246      Factory Response[ReadHoldingRegistersResponse: 3]
2019-01-31 12:35:03,202 Thread-1        DEBUG    rtu_framer     :110      Frame advanced, resetting header!!
2019-01-31 12:35:03,202 Thread-1        INFO     scratch_118    :64       Inside request fetched...
2019-01-31 12:35:03,203 Thread-1        INFO     scratch_118    :55       Reading holding registers of Modbus RTU device at address 127.0.0.1:/dev/ptyp0...
2019-01-31 12:35:03,203 Thread-1        DEBUG    __init__       :112      send: 0x1 0x3 0x0 0x0 0x0 0xa 0xc5 0xcd
2019-01-31 12:35:03,203 Thread-1        DEBUG    transaction    :418      Adding transaction 3
2019-01-31 12:35:04,207 Thread-1        DEBUG    rtu_framer     :175      Getting Frame - 0x3 0x14 0x0 0x2d 0x0 0x2c 0x0 0x2e 0x0 0x2d 0x0 0x2d 0x0 0x2e 0x0 0x29 0x0 0x2d 0x0 0x2d 0x0 0x2c
2019-01-31 12:35:04,207 Thread-1        DEBUG    factory        :246      Factory Response[ReadHoldingRegistersResponse: 3]
2019-01-31 12:35:04,208 Thread-1        DEBUG    rtu_framer     :110      Frame advanced, resetting header!!
2019-01-31 12:35:04,208 Thread-1        INFO     scratch_118    :64       Inside request fetched...
2019-01-31 12:35:04,208 Thread-1        INFO     scratch_118    :55       Reading holding registers of Modbus RTU device at address 127.0.0.1:/dev/ptyp0...
2019-01-31 12:35:04,208 Thread-1        DEBUG    __init__       :112      send: 0x1 0x3 0x0 0x0 0x0 0xa 0xc5 0xcd
2019-01-31 12:35:04,209 Thread-1        DEBUG    transaction    :418      Adding transaction 4
2019-01-31 12:35:05,213 Thread-1        DEBUG    rtu_framer     :175      Getting Frame - 0x3 0x14 0x0 0x2d 0x0 0x2c 0x0 0x2e 0x0 0x2d 0x0 0x2d 0x0 0x2e 0x0 0x29 0x0 0x2d 0x0 0x2d 0x0 0x2c
2019-01-31 12:35:05,213 Thread-1        DEBUG    factory        :246      Factory Response[ReadHoldingRegistersResponse: 3]
2019-01-31 12:35:05,214 Thread-1        DEBUG    rtu_framer     :110      Frame advanced, resetting header!!
2019-01-31 12:35:05,214 Thread-1        INFO     scratch_118    :64       Inside request fetched...
2019-01-31 12:35:05,214 Thread-1        INFO     scratch_118    :55       Reading holding registers of Modbus RTU device at address 127.0.0.1:/dev/ptyp0...
2019-01-31 12:35:05,214 Thread-1        DEBUG    __init__       :112      send: 0x1 0x3 0x0 0x0 0x0 0xa 0xc5 0xcd
2019-01-31 12:35:05,215 Thread-1        DEBUG    transaction    :418      Adding transaction 5
2019-01-31 12:35:06,218 Thread-1        DEBUG    rtu_framer     :175      Getting Frame - 0x3 0x14 0x0 0x2d 0x0 0x2c 0x0 0x2e 0x0 0x2d 0x0 0x2d 0x0 0x2e 0x0 0x29 0x0 0x2d 0x0 0x2d 0x0 0x2c
2019-01-31 12:35:06,218 Thread-1        DEBUG    factory        :246      Factory Response[ReadHoldingRegistersResponse: 3]
2019-01-31 12:35:06,219 Thread-1        DEBUG    rtu_framer     :110      Frame advanced, resetting header!!
2019-01-31 12:35:06,219 Thread-1        INFO     scratch_118    :64       Inside request fetched...
2019-01-31 12:35:06,219 Thread-1        INFO     scratch_118    :55       Reading holding registers of Modbus RTU device at address 127.0.0.1:/dev/ptyp0...
Sanju
  • 1,974
  • 1
  • 18
  • 33
  • After adding `reactor.callLater` in `connectionMade()`, it is not working. There is some relation of `endpoint` with `Factory` & `Protocol`. Do you have any idea about this ? With `callLater` only my `read()` method get executed but, it is unable to execute `self.read_holding_registers()` line. – OO7 Jan 31 '19 at 06:12
  • I have updated the complete code which works for me against a modbus simulator running locally. I have taken the liberty to add missing imports and some changes in the code (which is incomplete in the code snippet you have provided). I have also provided the logs for your reference. – Sanju Jan 31 '19 at 07:11
  • Thanks Sanju 4 quick response. Your updated code is working for me. It gives encoded data in response. So I used `BinaryPayloadDecoder.fromRegisters(response.registers, byteorder=Endian.Big, wordorder=Endian.Big)` to decode it. & it works like a charm. But, when I disconnect Modbus device, it's trying to fetch data & block on line `Reading holding registers of Modbus RTU device at address 127.0.0.1:/dev/ttyUSB0...`. A/c to me, it should raise an error `device disconnected`. I've overridden `clientConnectionLost` & `clientConnectionFailed` methods in `CustomModbusClientFactory` but, no success. – OO7 Jan 31 '19 at 08:29
  • With Modbus TCP device, connection lost & reconnects woks for me. But, not with Modbus RTU. Do you have any idea on this ? – OO7 Jan 31 '19 at 08:32
  • Glad it works. Could you please accept the answer in that case. Thanks – Sanju Jan 31 '19 at 12:41
  • Sure. I'll accept it. Do you have any idea on how to handle disconnection of Modbus RTU device & how can we reconnect it on availability of device ? – OO7 Jan 31 '19 at 13:04
  • This only works for serial device having `parity='N'`. Check my solution given below for serial device communication & you can also update device configuration as you required. I've tested it with PLC connected to raspberrypi. – OO7 Feb 07 '19 at 10:16
0

Below solution works for me. My device configurations are given below:

Device Configuration:
Baudrate=9600, Parity='E', Bytesize=8 Timeout=0.2
Protocol: RTU
PLC is connected to RaspberryPI using serial-to-USB converter.

Working Code:

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, timeout=0.2, 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 logger{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"]
        logger.info("Reading holding registers of Modbus RTU device at address {0}...".format(deviceIP + ":" + str(devicePort)))
        deferred = self.read_holding_registers(0, 1, unit=slaveAddress)
        deferred.addCallbacks(self.requestFetched, self.requestNotFetched)

    def requestNotFetched(self, error):
        logger.info("Error reading registers of Modbus RTU device : {0}".format(error))
        sleep(0.5)

    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_16bit_int()
        skipBytesCount += 2
        logger.info("Sensor updated to value '{0}'.".format(registerValue))
        reactor.callLater(5, self.read)  # @UndefinedVariable


readDevices({"ip": "127.0.0.1", "port": "/dev/ttyUSB0", "slaveAddress": 1})

Output:

2019-02-03 00:39:00,623 MainThread      INFO     TestRTU:26       Connecting to Modbus RTU device at address 127.0.0.1:/dev/ttyUSB0
2019-02-03 00:39:00,628 MainThread      INFO     TestRTU:73       Modbus RTU device connected at address logger127.0.0.1:/dev/ttyUSB0
2019-02-03 00:39:05,634 Thread-1        INFO     TestRTU:80       Reading holding registers of Modbus RTU device at address 127.0.0.1:/dev/ttyUSB0...
2019-02-03 00:39:05,676 Thread-1        INFO     TestRTU:89       Inside request fetched...
2019-02-03 00:39:05,677 Thread-1        INFO     TestRTU:95       Sensor updated to value '26'.

serialport.SerialPort.init(...) constructor is responsible for initializing all the required parameters for serial communication with device. You just have to update your device setting here & run the code. SerialModbusClient(modbusClientFactory, devicePort, reactor, baudrate=9600, bytesize=EIGHTBITS, parity=PARITY_EVEN, stopbits=STOPBITS_ONE, timeout=0.2, xonxoff=0, rtscts=0)

OO7
  • 2,785
  • 1
  • 21
  • 33