Saltar al contenido

Biblioteca modbus de Python

Después de tanto trabajar pudimos dar con la contestación de este rompecabezas que muchos lectores de nuestra web tienen. Si tienes algún detalle que compartir puedes dejar tu comentario.

Solución:

Casi al mismo tiempo me enfrenté al mismo problema: qué biblioteca elegir para la implementación del maestro modbus de Python, pero en mi caso para la comunicación en serie (modbus RTU), por lo que mis observaciones solo son válidas para modbus RTU.

En mi examen no presté demasiada atención a la documentación, pero los ejemplos para el maestro RTU en serie fueron más fáciles de encontrar para modbus-tk, sin embargo, todavía están en la fuente, no en una wiki, etc.

Manteniendo la historia corta:

Mínimo Modbus:

  • pros:
    • módulo ligero
    • el rendimiento puede ser aceptable para aplicaciones que lean ~ 10 registros
  • contras:
    • inaceptablemente (para mi aplicación) lento al leer ~ 64 registros
    • carga de CPU relativamente alta

pymodbus:

característica distintiva: se basa en la transmisión en serie (publicación del autor) y el tiempo de espera en serie debe configurarse dinámicamente, de lo contrario, el rendimiento será bajo (el tiempo de espera en serie debe ajustarse para obtener la respuesta más larga posible)

  • pros:
    • carga de CPU baja
    • rendimiento aceptable
  • contras:
    • incluso cuando el tiempo de espera se establece dinámicamente, el rendimiento es 2 veces menor en comparación con modbus-tk; si el tiempo de espera se deja en un valor constante, el rendimiento es mucho peor (pero el tiempo de consulta es constante)
    • sensible al hardware (como resultado de la dependencia del flujo de procesamiento desde el búfer en serie, creo) o puede haber un problema interno con las transacciones: puede obtener respuestas confusas si se realizan diferentes lecturas o lecturas / escrituras ~ 20 veces por segundo o más . Los tiempos de espera más prolongados ayudan, pero no siempre hacen que la implementación de pymodbus RTU en una línea en serie no sea lo suficientemente robusta para su uso en producción.
    • agregar soporte para la configuración de tiempo de espera de puerto serie dinámico requiere programación adicional: heredar la clase de cliente de sincronización base e implementar métodos de modificación de tiempo de espera de socket
    • La validación de respuestas no es tan detallada como en modbus-tk. Por ejemplo, en el caso de una caída del bus, solo se lanza una excepción, mientras que modbus-tk devuelve en la misma situación una dirección de esclavo incorrecta o un error de CRC que ayuda a identificar la causa raíz del problema (que puede ser un tiempo de espera demasiado corto, una terminación de bus incorrecta / falta de ella o suelo flotante, etc.)

modbus-tk:

Característica distintiva: prueba el búfer en serie para los datos, ensambla y devuelve la respuesta rápidamente.

  • pros
    • mejor actuacion; ~ 2 veces más rápido que pymodbus con tiempo de espera dinámico
  • contras:
    • aprox. Carga de CPU 4 veces mayor en comparación con pymodbus // se puede mejorar enormemente haciendo que este punto sea inválido; ver la sección EDITAR al final
    • La carga de la CPU aumenta para solicitudes más grandes // se puede mejorar enormemente haciendo que este punto sea inválido; ver la sección EDITAR al final
    • código no tan elegante como pymodbus

Durante más de 6 meses estuve usando pymodbus debido a la mejor relación rendimiento / carga de la CPU, pero las respuestas poco confiables se convirtieron en un problema grave a tasas de solicitud más altas y, finalmente, me mudé a un sistema integrado más rápido y agregué soporte para modbus-tk, que funciona mejor para mí.

Para los interesados ​​en los detalles

Mi objetivo era lograr un tiempo de respuesta mínimo.

configuración:

  • velocidad en baudios: 153600
    • en sincronía con el reloj de 16MHz del microcontrolador que implementa modbus esclavo)
    • mi bus rs-485 tiene solo 50 m
  • Convertidor FTDI FT232R y también serial sobre puente TCP (usando com4com como puente en modo RFC2217)
  • en el caso del convertidor de USB a serie, los tiempos de espera más bajos y los tamaños de búfer configurados para el puerto serie (para reducir la latencia)
  • adaptador auto-tx rs-485 (el bus tiene un estado dominante)

Escenario de caso de uso:

  • Sondeo 5, 8 o 10 veces por segundo con soporte para acceso asíncrono en el medio
  • Solicitudes de lectura / escritura de 10 a 70 registros

Rendimiento típico a largo plazo (semanas):

  • MinimalModbus: eliminado después de las pruebas iniciales
  • pymodbus: ~ 30ms para leer 64 registros; efectivamente hasta 30 solicitudes / seg
    • pero las respuestas no son confiables (en caso de acceso sincronizado desde múltiples subprocesos)
    • posiblemente haya una bifurcación segura para subprocesos en github, pero está detrás del maestro y no la he probado (https://github.com/xvart/pymodbus/network)
  • modbus-tk: ~ 16ms para leer 64 registros; efectivamente hasta 70 – 80 solicitudes / s para solicitudes más pequeñas

punto de referencia

código:

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()

resultados:

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

leyendo 100 x 64 registros:

sin ahorro de energía

timeout: 0.05 [s]
Serial(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(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(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]

máximo ahorro de energía

timeout: 0.05 [s]
Serial(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(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(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]

lectura de 100 x 10 registros:

sin ahorro de energía

timeout: 0.05 [s]
Serial(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(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(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]

máximo ahorro de energía

timeout: 0.05 [s]
Serial(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(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(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]

aplicación de la vida real:

Ejemplo de carga para puente modbus-rpc (~ 3% es causado por la parte del servidor RPC)

  • 5 x 64 registra lecturas sincrónicas por segundo y simultáneas

  • acceso asincrónico con tiempo de espera del puerto serie establecido en 0.018 s

    • modbus-tk

      • 10 registros: ‘currentCpuUsage’: 20.6, ‘ordersPerSec’: 73.2 // puede ser mejorado; consulte la sección EDITAR a continuación
      • 64 registros: ‘currentCpuUsage’: 31.2, ‘ordersPerSec’: 41.91 // puede ser mejorado; consulte la sección EDITAR a continuación
    • pymodbus:

      • 10 registros: ‘currentCpuUsage’: 5.0, ‘requestPerSec’: 36.88
      • 64 registros: ‘currentCpuUsage’: 5.0, ‘requestPerSec’: 34.29

EDITAR: la biblioteca modbus-tk se puede mejorar fácilmente para reducir el uso de la CPU. En la versión original, después de que se envía la solicitud y T3.5, el maestro de paso de suspensión ensambla la respuesta un byte a la vez. La elaboración de perfiles demostró que la mayor parte del tiempo se dedica al acceso al puerto serie. Esto se puede mejorar intentando leer la longitud esperada de datos del búfer en serie. De acuerdo con la documentación de pySerial, debería ser seguro (no cuelgue cuando falta la respuesta o es demasiado corta) si se establece el tiempo de espera:

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. 

después de modificar el `modbus_rtu.py ‘de la siguiente manera:

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

Después de la modificación de modbus-tk, la carga de la CPU en la aplicación de la vida real se redujo considerablemente sin una penalización significativa del rendimiento (aún mejor que pymodbus):

Ejemplo de carga actualizado para el puente modbus-rpc (~ 3% es causado por la parte del servidor RPC)

  • 5 x 64 registra lecturas sincrónicas por segundo y simultáneas

  • acceso asincrónico con tiempo de espera del puerto serie establecido en 0.018 s

    • modbus-tk

      • 10 registros: ‘currentCpuUsage’: 7.8, ‘requestPerSec’: 66.81
      • 64 registros: ‘currentCpuUsage’: 8.1, ‘requestPerSec’: 37.61
    • pymodbus:

      • 10 registros: ‘currentCpuUsage’: 5.0, ‘requestPerSec’: 36.88
      • 64 registros: ‘currentCpuUsage’: 5.0, ‘requestPerSec’: 34.29

Acabo de descubrir uModbus, y para implementarlo en algo como un Raspberry PI (u otro pequeño SBC), es un sueño. Es un paquete simple de capacidad única que no trae más de 10 dependencias como lo hace pymodbus.

Realmente depende de la aplicación que esté usando y de lo que esté tratando de lograr.

pymodbus es una biblioteca muy robusta. Funciona y le brinda muchas herramientas con las que trabajar. Pero puede resultar un poco intimidante cuando intentas usarlo. Me resultó difícil trabajar personalmente. Te ofrece la posibilidad de utilizar RTU y TCP / IP, ¡lo cual es genial!

MinimalModbus es una biblioteca muy simple. Terminé usando esto para mi aplicación porque hizo exactamente lo que necesitaba que hiciera. Solo hace comunicaciones RTU y, por lo que yo sé, lo hace bien. Nunca he tenido ningún problema con eso.

Nunca he investigado Modbus-tk, así que no sé dónde se encuentra.

Sin embargo, en última instancia, depende de cuál sea su aplicación. Al final descubrí que Python no era la mejor opción para mí.

Agradecemos que quieras añadir valor a nuestro contenido añadiendo tu veteranía en las ilustraciones.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *