pymodbus - easymodbus python
Biblioteca de modbus de Python (3)
Acabo de descubrir uModbus , y para la implementación en algo como una Raspberry PI (u otra SBC pequeña), es un sueño. Es un paquete simple y con capacidad única que no incluye más de 10 dependencias como lo hace pymodbus.
Tengo que controlar un dispositivo modbus con una interfaz serial. No tengo experiencia con modbus. Pero mi breve investigación reveló varias bibliotecas modbus
¿Cuáles son las ventajas / desventajas, hay incluso mejores alternativas?
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.
manteniendo la larga historia corta:
MinimalModbus:
- pros:
- módulo ligero
- el rendimiento puede ser aceptable para aplicaciones que leen ~ 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 del flujo de procesamiento desde el búfer en serie, creo) o puede haber un problema interno con las transacciones: puede confundirse 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.
- agregar soporte para la configuración del tiempo de espera del puerto serie dinámico requiere programación adicional: heredar la clase del cliente de sincronización base e implementar los métodos de modificación del tiempo de espera del socket
- La validación de respuestas no es tan detallada como en modbus-tk. Por ejemplo, en el caso de una caída de 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 / falta de bus del bus) o suelo flotante, etc.)
modbus-tk:
característica distintiva: sondea el búfer en serie para los datos, ensambla y devuelve la respuesta rápidamente.
- pros
- Mejor presentación; ~ 2 veces más rápido que el pymodbus con tiempo de espera dinámico
- contras:
- aprox. 4 veces más carga de la CPU en comparación con el pymodbus // se puede mejorar enormemente haciendo que este punto no sea válido; ver la sección EDITAR al final
- La carga de la CPU aumenta para solicitudes más grandes // puede mejorarse enormemente haciendo que este punto no sea válido; ver la sección EDITAR al final
- código no tan elegante como el 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, pasé 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 objetivo era lograr un tiempo de respuesta mínimo.
preparar:
- Velocidad en baudios: 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 caso de convertidor USB a serie, los tiempos de espera más bajos y los tamaños de memoria intermedia 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 asincrónico en el medio
- Solicitudes para leer / escribir de 10 a 70 registros
Rendimiento típico a largo plazo (semanas):
- MinimalModbus: se 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 hay 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 %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<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]
máximo ahorro de energía
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]
leyendo 100 x 10 registros:
sin ahorro de energía
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]
máximo ahorro de energía
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]
aplicación de 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 abajo
- 64 registros: {''currentCpuUsage'': 31.2, ''requestPerSec'': 41.91} // se puede mejorar; vea la sección EDITAR abajo
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 superado 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 tratando de leer la longitud esperada de los datos del búfer en serie. De acuerdo con la documentación de pySerial , debe ser seguro (no colgar cuando la respuesta falta 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 de 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 qué aplicación esté usando y de lo que esté 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 biblioteca muy simple. Terminé usando esto para mi aplicación porque hizo exactamente lo que necesitaba. Solo hace comunicaciones RTU, y lo hace bien hasta donde sé. Nunca he tenido ningún problema con eso.
Nunca he visto 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í.