Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python modbus library

Tags:

python

modbus

I have to control a modbus device with a serial interface. I've got not experience with modbus. But my short research revealed several modbus libraries

  • pymodbus
  • MinimalModbus
  • Modbus-tk
  • uModbus

What are the advantages/disadvantages, are there even better alternatives?

like image 366
P3trus Avatar asked Jun 13 '13 07:06

P3trus


People also ask

What is Modbus library?

A Modbus library for Linux, Mac OS X, FreeBSD, QNX and Win32 A free software library to send/receive data according to the Modbus protocol. This library is written in C and supports RTU (serial) and TCP (Ethernet) communications.

What is Modbus RTU?

Modbus-RTU (Remote Terminal Unit) means that the Modbus protocol is used on top of a serial line with an RS-232, RS-485 or similar physical interface. Numerous automation systems have Modbus-RTU interfaces for communication.

What is Modbus minimal?

MinimalModbus is an easy-to-use Python module for talking to instruments (slaves) from a computer (master) using the Modbus protocol, and is intended to be running on the master.


1 Answers

About the same time I faced the same problem - which library to choose for python modbus master implementation but in my case for serial communication (modbus RTU) so my observations are only valid for modbus RTU.

In my examination I didn't pay too much attention to documentation but examples for serial RTU master were easiest to find for modbus-tk however still in source not on a wiki etc.

keeping long story short:

MinimalModbus:

  • pros:
    • lightweight module
    • performance may be acceptable for applications reading ~10 registers
  • cons:
    • unacceptably (for my application) slow when reading ~64 registers
    • relatively high CPU load

pymodbus:

distinctive feature: relies on serial stream (post by the author) and serial timeout must be dynamically set otherwise performance will be low (serial timeout must be adjusted for the longest possible response)

  • pros:
    • low CPU load
    • acceptable performance
  • cons:
    • even when timeout is dynamically set performance is 2 x lower compared to modbus-tk; if timeout is left at a constant value performance is much worse (but query time is constant)
    • sensitive to hardware (as a result of dependency on processing stream from serial buffer I think) or there may be internal problem with transactions: you can get responses mixed-up if different reads or reads/writes are performed ~20 times per second or more. Longer timeouts help but not always making pymodbus RTU implementation over a serial line not enough robust for use in production.
    • adding support for dynamic serial port timeout setting requires additional programming: inheriting base sync client class and implementing socket timeout modification methods
    • responses validation not as detailed as in modbus-tk. For example in case of a bus decay only exception is thrown whereas modbus-tk returns in the same situation wrong slave address or CRC error which helps identifying root cause of the problem (which may be too short timeout, wrong bus termination / lack thereof or floating ground etc.)

modbus-tk:

distinctive feature: probes serial buffer for data, assembles and returns response quickly.

  • pros
    • best performance; ~2 x times faster than pymodbus with dynamic timeout
  • cons:
    • approx. 4 x higher CPU load compared to pymodbus // can be greately improved making this point invalid; see EDIT section at the end
    • CPU load increases for larger requests // can be greately improved making this point invalid; see EDIT section at the end
    • code not as elegant as pymodbus

For over 6 months I was using pymodbus due to best performance / CPU load ratio but unreliable responses became a serious issue at higher request rates and eventually I moved to faster embedded system and added support for modbus-tk which works best for me.

For those interested in details

My goal was to achieve minimum response time.

setup:

  • baudrate: 153600
    • in sync with 16MHz clock of the microcontroller implementing modbus slave)
    • my rs-485 bus has only 50m
  • FTDI FT232R converter and also serial over TCP bridge (using com4com as a bridge in RFC2217 mode)
  • in case of USB to serial converter lowest timeouts and buffer sizes configured for serial port (to lower latency)
  • auto-tx rs-485 adapter (bus has a dominant state)

Use case scenario:

  • Polling 5, 8 or 10 times a second with support for asynchronous access in between
  • Requests for reading/writing 10 to 70 registers

Typical long-term (weeks) performance:

  • MinimalModbus: dropped after initial tests
  • pymodbus: ~30ms to read 64 registers; effectively up to 30 requests / sec
    • but responses unreliable (in case of synchronized access from multiple threads)
    • there is possibly a threadsafe fork on github but it's behind the master and I haven't tried it (https://github.com/xvart/pymodbus/network)
  • modbus-tk: ~16ms to read 64 registers; effectively up to 70 - 80 requests / sec for smaller requests

benchmark

code:

import time import traceback import serial import modbus_tk.defines as tkCst import modbus_tk.modbus_rtu as tkRtu  import minimalmodbus as mmRtu  from pymodbus.client.sync import ModbusSerialClient as pyRtu  slavesArr = [2] iterSp = 100 regsSp = 10 portNbr = 21 portName = 'com22' baudrate = 153600  timeoutSp=0.018 + regsSp*0 print "timeout: %s [s]" % timeoutSp   mmc=mmRtu.Instrument(portName, 2)  # port name, slave address mmc.serial.baudrate=baudrate mmc.serial.timeout=timeoutSp  tb = None errCnt = 0 startTs = time.time() for i in range(iterSp):   for slaveId in slavesArr:     mmc.address = slaveId     try:         mmc.read_registers(0,regsSp)     except:         tb = traceback.format_exc()         errCnt += 1 stopTs = time.time() timeDiff = stopTs  - startTs  mmc.serial.close()  print mmc.serial  print "mimalmodbus:\ttime to read %s x %s (x %s regs): %.3f [s] / %.3f [s/req]" % (len(slavesArr),iterSp, regsSp, timeDiff, timeDiff/iterSp) if errCnt >0:     print "   !mimalmodbus:\terrCnt: %s; last tb: %s" % (errCnt, tb)    pymc = pyRtu(method='rtu', port=portNbr, baudrate=baudrate, timeout=timeoutSp)  errCnt = 0 startTs = time.time() for i in range(iterSp):   for slaveId in slavesArr:     try:         pymc.read_holding_registers(0,regsSp,unit=slaveId)     except:         errCnt += 1         tb = traceback.format_exc() stopTs = time.time() timeDiff = stopTs  - startTs print "pymodbus:\ttime to read %s x %s (x %s regs): %.3f [s] / %.3f [s/req]" % (len(slavesArr),iterSp, regsSp, timeDiff, timeDiff/iterSp) if errCnt >0:     print "   !pymodbus:\terrCnt: %s; last tb: %s" % (errCnt, tb) pymc.close()   tkmc = tkRtu.RtuMaster(serial.Serial(port=portNbr, baudrate=baudrate)) tkmc.set_timeout(timeoutSp)  errCnt = 0 startTs = time.time() for i in range(iterSp):   for slaveId in slavesArr:     try:         tkmc.execute(slaveId, tkCst.READ_HOLDING_REGISTERS, 0,regsSp)     except:         errCnt += 1         tb = traceback.format_exc() stopTs = time.time() timeDiff = stopTs  - startTs print "modbus-tk:\ttime to read %s x %s (x %s regs): %.3f [s] / %.3f [s/req]" % (len(slavesArr),iterSp, regsSp, timeDiff, timeDiff/iterSp) if errCnt >0:     print "   !modbus-tk:\terrCnt: %s; last tb: %s" % (errCnt, tb) tkmc.close() 

results:

platform: P8700 @2.53GHz WinXP sp3 32bit Python 2.7.1 FTDI FT232R series 1220-0 FTDI driver 2.08.26 (watch out for possible issues with 2.08.30 version on Windows) pymodbus version 1.2.0 MinimalModbus version 0.4 modbus-tk version 0.4.2 

reading 100 x 64 registers:

no power saving

timeout: 0.05 [s] Serial<id=0xd57330, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.05, xonxoff=False, rtscts=False, dsrdtr=False) mimalmodbus:    time to read 1 x 100 (x 64 regs): 9.135 [s] / 0.091 [s/req] pymodbus:       time to read 1 x 100 (x 64 regs): 6.151 [s] / 0.062 [s/req] modbus-tk:      time to read 1 x 100 (x 64 regs): 2.280 [s] / 0.023 [s/req]  timeout: 0.03 [s] Serial<id=0xd57330, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.03, xonxoff=False, rtscts=False, dsrdtr=False) mimalmodbus:    time to read 1 x 100 (x 64 regs): 7.292 [s] / 0.073 [s/req] pymodbus:       time to read 1 x 100 (x 64 regs): 3.170 [s] / 0.032 [s/req] modbus-tk:      time to read 1 x 100 (x 64 regs): 2.342 [s] / 0.023 [s/req]   timeout: 0.018 [s] Serial<id=0xd57330, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.018, xonxoff=False, rtscts=False, dsrdtr=False) mimalmodbus:    time to read 1 x 100 (x 64 regs): 4.481 - 7.198 [s] / 0.045 - 0.072 [s/req] pymodbus:       time to read 1 x 100 (x 64 regs): 3.045 [s] / 0.030 [s/req] modbus-tk:      time to read 1 x 100 (x 64 regs): 2.342 [s] / 0.023 [s/req] 

maximum power saving

timeout: 0.05 [s] Serial<id=0xd57330, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.05, xonxoff=False, rtscts=False, dsrdtr=False) mimalmodbus:    time to read 1 x 100 (x 64 regs): 10.289 [s] / 0.103 [s/req] pymodbus:       time to read 1 x 100 (x 64 regs):  6.074 [s] / 0.061 [s/req] modbus-tk:      time to read 1 x 100 (x 64 regs):  2.358 [s] / 0.024 [s/req]  timeout: 0.03 [s] Serial<id=0xd57330, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.03, xonxoff=False, rtscts=False, dsrdtr=False) mimalmodbus:    time to read 1 x 100 (x 64 regs): 8.166 [s] / 0.082 [s/req] pymodbus:       time to read 1 x 100 (x 64 regs): 4.138 [s] / 0.041 [s/req] modbus-tk:      time to read 1 x 100 (x 64 regs): 2.327 [s] / 0.023 [s/req]  timeout: 0.018 [s] Serial<id=0xd57330, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.018, xonxoff=False, rtscts=False, dsrdtr=False) mimalmodbus:    time to read 1 x 100 (x 64 regs): 7.776 [s] / 0.078 [s/req] pymodbus:       time to read 1 x 100 (x 64 regs): 3.169 [s] / 0.032 [s/req] modbus-tk:      time to read 1 x 100 (x 64 regs): 2.342 [s] / 0.023 [s/req] 

reading 100 x 10 registers:

no power saving

timeout: 0.05 [s] Serial<id=0xd56350, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.05, xonxoff=False, rtscts=False, dsrdtr=False) mimalmodbus:    time to read 1 x 100 (x 10 regs): 6.246 [s] / 0.062 [s/req] pymodbus:       time to read 1 x 100 (x 10 regs): 6.199 [s] / 0.062 [s/req] modbus-tk:      time to read 1 x 100 (x 10 regs): 1.577 [s] / 0.016 [s/req]  timeout: 0.03 [s] Serial<id=0xd56350, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.03, xonxoff=False, rtscts=False, dsrdtr=False) mimalmodbus:    time to read 1 x 100 (x 10 regs): 3.088 [s] / 0.031 [s/req] pymodbus:       time to read 1 x 100 (x 10 regs): 3.143 [s] / 0.031 [s/req] modbus-tk:      time to read 1 x 100 (x 10 regs): 1.533 [s] / 0.015 [s/req]  timeout: 0.018 [s] Serial<id=0xd56350, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.018, xonxoff=False, rtscts=False, dsrdtr=False) mimalmodbus:    time to read 1 x 100 (x 10 regs): 3.066 [s] / 0.031 [s/req] pymodbus:       time to read 1 x 100 (x 10 regs): 3.006 [s] / 0.030 [s/req] modbus-tk:      time to read 1 x 100 (x 10 regs): 1.533 [s] / 0.015 [s/req] 

maximum power saving

timeout: 0.05 [s] Serial<id=0xd56350, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.05, xonxoff=False, rtscts=False, dsrdtr=False) mimalmodbus:    time to read 1 x 100 (x 10 regs): 6.386 [s] / 0.064 [s/req] pymodbus:       time to read 1 x 100 (x 10 regs): 5.934 [s] / 0.059 [s/req] modbus-tk:      time to read 1 x 100 (x 10 regs): 1.499 [s] / 0.015 [s/req]  timeout: 0.03 [s] Serial<id=0xd56350, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.03, xonxoff=False, rtscts=False, dsrdtr=False) mimalmodbus:    time to read 1 x 100 (x 10 regs): 3.139 [s] / 0.031 [s/req] pymodbus:       time to read 1 x 100 (x 10 regs): 3.170 [s] / 0.032 [s/req] modbus-tk:      time to read 1 x 100 (x 10 regs): 1.562 [s] / 0.016 [s/req]  timeout: 0.018 [s] Serial<id=0xd56350, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.018, xonxoff=False, rtscts=False, dsrdtr=False) mimalmodbus:    time to read 1 x 100 (x 10 regs): 3.123 [s] / 0.031 [s/req] pymodbus:       time to read 1 x 100 (x 10 regs): 3.060 [s] / 0.031 [s/req] modbus-tk:      time to read 1 x 100 (x 10 regs): 1.561 [s] / 0.016 [s/req] 

real-life application:

Load example for modbus-rpc bridge (~3% is caused by RPC server part)

  • 5 x 64 registers synchronous reads per second and simultaneous

  • asynchronous access with serial port timeout set to 0.018 s

    • modbus-tk

      • 10 regs: {'currentCpuUsage': 20.6, 'requestsPerSec': 73.2} // can be improved; see EDIT section below
      • 64 regs: {'currentCpuUsage': 31.2, 'requestsPerSec': 41.91} // can be improved; see EDIT section below
    • pymodbus:

      • 10 regs: {'currentCpuUsage': 5.0, 'requestsPerSec': 36.88}
      • 64 regs: {'currentCpuUsage': 5.0, 'requestsPerSec': 34.29}

EDIT: the modbus-tk library can be easily improved to reduce the CPU usage. In the original version after request is sent and T3.5 sleep passed master assembles response one byte at a time. Profiling proved most od the time is spent on serial port access. This can be improved by trying to read the expected length of data from the serial buffer. According to pySerial documentation it should be safe (no hang up when response is missing or too short) if timeout is set:

read(size=1) Parameters: size – Number of bytes to read. Returns:    Bytes read from the port. Read size bytes from the serial port. If a timeout is set it may return less characters as    requested. With no timeout it will block until the requested number of bytes is read.  

after modifying the `modbus_rtu.py' in the following way:

def _recv(self, expected_length=-1):      """Receive the response from the slave"""      response = ""      read_bytes = "dummy"      iterCnt = 0      while read_bytes:          if iterCnt == 0:              read_bytes = self._serial.read(expected_length)  # reduces CPU load for longer frames; serial port timeout is used anyway           else:              read_bytes = self._serial.read(1)          response += read_bytes          if len(response) >= expected_length >= 0:              #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          iterCnt += 1 

After modbus-tk modification the CPU load in the real-life application dropped considerably without significant performance penalty (still better than pymodbus):

Updated load example for modbus-rpc bridge (~3% is caused by RPC server part)

  • 5 x 64 registers synchronous reads per second and simultaneous

  • asynchronous access with serial port timeout set to 0.018 s

    • modbus-tk

      • 10 regs: {'currentCpuUsage': 7.8, 'requestsPerSec': 66.81}
      • 64 regs: {'currentCpuUsage': 8.1, 'requestsPerSec': 37.61}
    • pymodbus:

      • 10 regs: {'currentCpuUsage': 5.0, 'requestsPerSec': 36.88}
      • 64 regs: {'currentCpuUsage': 5.0, 'requestsPerSec': 34.29}
like image 79
Mr. Girgitt Avatar answered Sep 21 '22 23:09

Mr. Girgitt