Biblioteca modus Python

Tengo que controlar un dispositivo modbus con una interfaz serial. No tengo experiencia con modbus. Pero mi breve investigación reveló varias bibliotecas modbus

  • pymodbus
  • MinimalModbus
  • Modbus-tk
  • uModbus

¿Cuáles son las ventajas / desventajas, hay alternativas aún mejores?

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

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

mantener larga historia corta:

MinimalModbus:

  • 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 el flujo de serie ( publicación por el autor ) y el tiempo de espera de serie debe configurarse dinámicamente; de ​​lo contrario, el rendimiento será bajo (el tiempo de espera de serie debe ajustarse para la respuesta más larga posible)

  • pros:
    • baja carga de CPU
    • 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 en el flujo de procesamiento desde el búfer en serie, creo) o puede haber un problema interno con las transacciones: puede mezclar las respuestas si se realizan diferentes lecturas o lecturas / escrituras ~ 20 veces por segundo o más . Los tiempos de espera más largos ayudan, pero no siempre hacen que la implementación de Pymodbus RTU sobre una línea serie no sea lo suficientemente robusta para su uso en producción.
    • la adición de soporte para la configuración del tiempo de espera de puerto serie dynamic requiere una progtwigció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 las 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, falta de terminación del bus o falta suelo flotante etc.)

modbus-tk:

característica distintiva: prueba el búfer serial para datos, ensambla y devuelve la respuesta rápidamente.

  • pros
    • Mejor presentación; ~ 2 veces más rápido que pymodbus con tiempo de espera dynamic
  • contras:
    • aprox. 4 veces mayor carga de CPU en comparación con pymodbus // se puede mejorar enormemente haciendo que este punto no sea válido; ver la sección EDITAR al final
    • Los aumentos de carga de CPU para solicitudes más grandes // pueden mejorarse enormemente haciendo que este punto no sea vá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 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 añadí soporte para modbus-tk, que funciona mejor para mí.

Para aquellos interesados ​​en detalles.

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

preparar:

  • Baudrate: 153600
    • sincronizado con el reloj de 16MHz del microcontrolador que implementa el esclavo Modbus)
    • mi bus rs-485 tiene solo 50m
  • Convertidor FTDI FT232R y también puente serie sobre TCP (utilizando com4com como puente en modo RFC2217)
  • en el caso de los tiempos de espera más bajos del convertidor de USB a serie 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)

Caso de uso del caso:

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

Rendimiento típico a largo plazo (semanas):

  • MinimalModbus: cayó después de las pruebas iniciales
  • pymodbus: ~ 30ms para leer 64 registros; efectivamente hasta 30 peticiones / seg.
    • pero las respuestas no son confiables (en caso de acceso sincronizado desde múltiples hilos)
    • posiblemente haya un tenedor seguro para hilos en github pero está detrás del maestro y no lo he probado ( https://github.com/xvart/pymodbus/network )
  • modbus-tk: ~ 16ms para leer 64 registros; efectivamente hasta 70 – 80 solicitudes / seg 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 %sx %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 %sx %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 %sx %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] 

leyendo 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 en la vida real:

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

  • 5 x 64 registros de lecturas síncronas por segundo y simultáneas

  • Acceso asíncrono con tiempo de espera de puerto serie establecido en 0.018 s

    • modbus-tk

      • 10 registros: {‘currentCpuUsage’: 20.6, ‘requestPerSec’: 73.2} // se puede mejorar; vea la sección EDITAR a continuación
      • 64 registros: {‘currentCpuUsage’: 31.2, ‘requestPerSec’: 41.91} // se puede mejorar; vea 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 el maestro T3.5 ha pasado la respuesta ensambla la respuesta un byte a la vez. Los perfiles demostraron que la mayor parte del tiempo se invierte en el acceso al puerto serie. Esto se puede mejorar al intentar leer la longitud esperada de los datos del búfer en serie. Según la documentación de pySerial , debe ser seguro (no colgar cuando falta la respuesta o es demasiado breve) 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 en la vida real disminuyó considerablemente sin una penalización de rendimiento significativa (aún mejor que pymodbus):

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

  • 5 x 64 registros de lecturas síncronas por segundo y simultáneas

  • Acceso asíncrono con tiempo de espera de 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}

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

pymodbus es una librería muy robusta. Funciona y te da muchas herramientas para trabajar. Pero puede resultar un poco intimidante cuando intentas usarlo. Me resultó difícil trabajar personalmente. Le ofrece la posibilidad de utilizar tanto RTU como TCP / IP, ¡lo cual es genial!

MinimalModbus es una librería muy simple. Terminé usando esto para mi aplicación porque hizo exactamente lo que necesitaba hacer. Solo hace comunicaciones RTU, y lo hace bien hasta donde sé. Nunca he tenido ningún problema con eso.

Nunca he buscado en Modbus-tk, así que no sé dónde está.

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í.

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