Search

Adquiriendo datos con Python y dispositivos ModBus

Vamos a empezar el año cambiando de tercio. Hoy vamos a ver cómo acceder a las lecturas de un dispositivo Modbus utilizando Python y su librería pymodbus.

En las comunicaciones industriales se puede hablar de una estructura con tres niveles:

  • En el nivel más bajo estaría el bus de campo. Es el nivel más cercano al proceso que se quiere controlar y/o monitorizar. Y es aquí donde PLCs (controladores lógicos programables), equipos de medida, y otros pequeños automatismos se integran para comunicarse entre sí con el fin de supervisar un determinado proceso.  A todo este conjunto de aparatitos se le llama célula de fabricación.
  • Por encima del nivel anterior estaría el nivel de LAN. Es en éste nivel dónde se comunican entre sí varias células de fabricación.
  • Por último tendríamos el nivel LAN/WAN. Sería el nivel más próximo al área de gestión. Este nivel se encarga de integrar los niveles anteriores en una estructura de planta industrial o incluso de múltiples plantas en diferentes emplazamientos. Aquí es donde se centralizaría el control y supervisión de todos los procesos a través de, por ejemplo, sistemas SCADA, y se incorporarían bases de datos.

Estos tres niveles se pueden representar en lo que se llama pirámide CIM (Computer Integrated Manufacturing).

Nosotros nos vamos a quedar en el nivel más bajo, en el bus de campo. Como he dicho antes es el nivel más sencillo y cercano al proceso a controlar. Un bus de campo es una red de comunicación entre los diferentes dispositivos que están implicados en el control y supervisión de un proceso, y que permite intercambiar órdenes y datos entre dichos dispositivos. Dentro de esta red, los dispositivos podrán ser todos pertenecientes a un mismo fabricante o a distintos, pero lo importante es que se puedan comunicar entre ellos a través de un protocolo reconocido por todos ellos, independientemente del fabricante del aparato.

Uno de los buses de campo más extendidos actualmente es MODBUS. Éste fue diseñado por la empresa Modicon en 1979 para implementarlo en sus PLCs, y está basado en una arquitectura maestro/esclavo (RTU) o cliente/servidor (TCP/IP). Realmente la denominación como bus de campo a MODBUS no es correcta, ya que MODBUS es un protocolo. Sin embargo, en la industria se suele hablar de MODBUS como un estándar de bus de campo. Voy a hacer un resumen rápido de sus características:

  • Es público y gratuito
  • El medio físico de conexión puede ser un bus RS-485 o RS-422.
  • Las velocidades de transmisión van desde los 75 a los 19200 baudios.
  • La distancia máxima entre dispositivos puede alcanzar hasta los 1200 metros sin necesidad de usar repetidores.
  • La estructura lógica es del tipo maestro-esclavo.
  • El número máximo de dispositivos es de 63 esclavos más un dispositivo maestro.
  • La codificación de datos dentro de la trama puede hacerse en modo ASCII o en RTU (Remote Transmission Unit).
  • Los campos de la trama del mensaje son los siguientes:
    • Número de dispositivo (1 byte): del 0 a 255. La dirección 0 es para mensajes broadcast.
    • Código de función (1 byte): Dependiendo de la función podemos transmitir datos u órdenes al esclavo.
    • Campo de subfunciones/datos (n bytes): Aquí van los parámetros necesarios para ejecutar la función anterior. Pueden palabras a leer o escribir, número de bits, etc…
    • Palabra de control de errores (2 bytes): En código ASCII es el CRC. Para el caso de codificación RTU el CRC se calcula mediante una fórmula.

MODBUS: Funciones básicas y códigos de operación

Para nuestro ejemplo vamos a usar una sonda de temperatura y humedad (data sheet de la sonda).

Para poder conectarme a ella y leer las temperaturas, voy a utilizar,  además de la propia sonda, un conversor de USB a RS-485, un pc con Ubuntu como SO, python y la librería pymodbus.

Lo primero que debemos hacer al conectar el conversor al puerto USB es verificar si se ha detectado. Para ello podemos listar los puertos serie ejecutando el siguiente comando en la consola:

dmesg | grep tty

[    0.000000] console [tty0] enabled
[   65.424911] usb 1-1: FTDI USB Serial Device converter now attached to ttyUSB0
[  445.927399] ftdi_sio ttyUSB0: FTDI USB Serial Device converter now disconnected from ttyUSB0
[  449.591797] usb 1-1: FTDI USB Serial Device converter now attached to ttyUSB0
[  634.484412] ftdi_sio ttyUSB0: FTDI USB Serial Device converter now disconnected from ttyUSB0
[  651.059646] usb 1-1: FTDI USB Serial Device converter now attached to ttyUSB0
[ 1234.073714] ftdi_sio ttyUSB0: FTDI USB Serial Device converter now disconnected from ttyUSB0
[ 1239.842671] usb 1-4: FTDI USB Serial Device converter now attached to ttyUSB0

Así podemos ver que el puerto que está utilizando el conversor es el ttyUSB0.

El nombre completo de este puerto es /dev/ttyUSB0, que corresponde al Conversor USB-serie 1. Es posible que tengas que cambiar los permisos mediante estos comandos:

sudo adduser yourUser dialout
sudo chmod a+rw /dev/ttyUSB0

Vale. Vamos a empezar a escribir nuestro código.

Lo primero que vamos a hacer es escribir una función que nos va a devolver el mapa de memoria de la sonda. ¿Qué es el mapa de memoria?. Es una tabla de direcciones en las que se puede acceder a los diferentes datos de la sonda. En un dispositivo básicamente podemos encontrar estos 4 bloques:

  • Salidas digitales (coils).
  • Entradas digitales (inputs).
  • Salidas analógicas (holding registers).
  • Entradas analógicas (input registers).

En el caso de nuestra sonda de temperatura y humedad, vamos a acceder a las direcciones de memoria de las salidas analógicas (holding registers).

Mapa de memoria Modbus

Según esto nos vamos a crear una función que devuelva un diccionario con tres claves-valor. Una para las direcciones de las salidas tipo integer, correspondientes a la configuración Modbus de la sonda. Otra para las direcciones de las salidas tipo float, correspondientes a las medidas de la sonda. Y una última para los valores por defecto de la configuración Modbus de la sonda. Nos quedaría algo tal que así:

def step_th():
    memo_integers = {'identificador': 4000,
                     'nper': 4001,
                     'velocidad': 4002,
                     'datos comunication': 4003,
                     'validar datos com': 4004,
                     'tiempo promedio medidas': 4005,
                     'borrado max y min': 4006}

    memo_floats = {'identificador 2': 7000,
                   'temperatura': 7002,
                   'humedad relativa': 7004,
                   'punto de rocio': 7006,
                   'humedad absoluta': 7008,
                   'temp minima': 7010,
                   'temp maxima': 7012,
                   'humedad relativa minima': 7014,
                   'humedad relativa maxima': 7016,
                   'punto rocio minimo': 7018,
                   'punto rocio maximo': 7020,
                   'humedad absoluta minima': 7022,
                   'humedad absoluta maxima': 7024}

    default_config = {'method': 'rtu',
                      'bitstop': 1,
                      'bytesize': 8,
                      'parity': 'N',
                      'baudrate': 9600,
                      'timeout': 3,
                      'nper':1}

    return {'memo_Integers': memo_integers,
            'memo_Floats': memo_floats,
            'default_config': default_config}

Una vez que tenemos los distintos diccionarios definidos, vamos a crearnos un script que se conecte a la sonda, lea los valores y los guarde en una base de datos. Yo voy a usar MySQL.

Lo primero que vamos a hacer es una función para realizar la conexión a la base de datos y la inserción de los valores leídos de la sonda.

Para ello crearemos el archivo mysqlConnect.py y dentro escribiremos la siguiente función:

def insertQuery(data_list):
    import mysql.connector
    from datetime import datetime

    current_date = datetime.now()
    formatted_date = current_date.strftime('%Y-%m-%d %H:%M:%S')
    data_list.append(formatted_date)

    try:
        connection = mysql.connector.connect(host='yourhost_maybe_localhost',
                                            database='nameofyourdatabase',
                                            user='youruser',
                                            password='yourpassword')
        cursor = connection.cursor()
        sql_query = """INSERT INTO `th_probe`
                        (`humedad_abs`, `humedad_abs_max`, `humedad_abs_min`,
                        `humedad_rel`, `humedad_rel_max`, `humedad_rel_min`,
                        `punto_rocio`, `punto_rocio_max`, `punto_rocio_min`,
                        `temperaura`, `temperatura_max`, `temperatura_min`,
                        `timestamp`) VALUES (%s, %s, %s,
                                            %s, %s, %s, 
                                            %s, %s, %s
                                            %s, %s, %s,
                                            %s)"""
        cursor.execute(sql_query, data_list)
        connection.commit()
        print("Datos grabados correctamente")
    except mysql.connector.Error as error:
        print("La insercion ha fallado. Error {}".format(error))
    finally:
        if connection.is_connected():
            cursor.close()
            connection.close()
            print("Conexion con MySQL cerrada")

Ésta función recibe como argumento una lista de valores, y lo que hace es conectarse a la base de datos e insertar en una tabla esa lista de valores, creando un nuevo registro.

Bien. Vamos ahora a escribir el script principal, que será el que realiza la conexión con la sonda, la lectura de los valores, y llamará a la función insertQuery para guardarlos datos obtenidos en la base de datos MySQL.

Empezamos importando las librerías necesarias, y creando una serie de variables necesarias para realizar la conexión con la sonda.

from pymodbus.client.sync import ModbusSerialClient as ModbusClient #initialize a serial RTU client instance
from stepTHconf import step_th
import numpy as np
import struct
from mysqlConnect import insertQuery

method = step_th()['default_config']['method']
stopbits = step_th()['default_config']['stopbits']
bytesize = step_th()['default_config']['bytesize']
parity = step_th()['default_config']['parity']
baudrate = step_th()['default_config']['baudrate']
timeout = step_th()['default_config']['timeout']
nper = step_th()['default_config']['nper']
port = '/dev/ttyUSB0'

client = ModbusClient(method=method,
                      stopbits=stopbits,
                      bytesize=bytesize,
                      parity=parity,
                      baudrate=baudrate,
                      timeout=timeout,
                      port=port)

connection = client.connect()

Para la lectura de los datos, utilizaremos la función read_holding_registers.

Ésta admite como parámetros, la dirección desde la que se empieza a leer, el número de registros a leer, y el número de periférico del dispositivo al que se le pregunta, en este caso, nuestra sonda, que lleva el número de periférico 1.

Empezaremos leyendo los valores de los registros correspondientes al diccionario memo_integers, que hemos creado anteriormente, y que contiene los datos de configuración de la sonda. En este caso, sólo leeremos el primer registro.

if connection:
    #Leemos los valores de los registros tipo integer
    #correspondientes a la configuración de la sonda
    print("memo_Integers values")
    for key, value in step_th()['memo_Integers'].items():
        rr = client.read_holding_registers(value, 1, unit=0x01)
        if not rr.isError():
            val = rr.registers[0]
            print('{}: {}'.format(key, val))
        else:
            print('{}: error'.format(key))

A continuación, leeremos los valores de las medidas, cuyas direcciones cogemos del diccionario memo_floats. En este caso, al ser un tipo de dato float, tenemos que leer, por un lado el registro 1 y por otro el registro 2. Necesitaremos ambos para obtener el valor . Para ello añadimos el siguiente bloque de código:

#Leemos los valores de los registros tipo float
#correspondientes a las medidas de la sonda
    print("memo_floats values")
    measure_names=[]
    values=[]
    for key, value in sorted(step_th()['memo_Floats'].items()):
        rr1 = client.read_holding_registers(value, 1, unit=0x01)
        rr2 = client.read_holding_registers(value, 2, unit=0x01)
        if not rr1.isError() and not rr2.isError():
            print('{}: {};{}'.format(key, rr1.registers[0], rr2.registers[0]))
            bin_number = '0' + str(np.base_repr(rr2.registers[0], base=2)) \
                         + '0' + str(np.base_repr(rr1.registers[0], base=2))
            f = int(bin_number, 2)
            value = round(struct.unpack('f', struct.pack('I', f))[0], 1)
            print("#"*20)
            print('{}:{}'.format(key, struct.unpack('f', struct.pack('I', f))[0]))
            print('#'*20)
            values.append(value)
            measure_names.append(key)
        else:
            print('{}: error'.format(key)

Si hacemos un print del resultado de esas lecturas, que hemos llamado rr1 y rr2, podemos ver sus valores. Por ejemplo nos vamos a fijar en la humedad absoluta:

temperatura: 23040;16808
####################
temperatura:21.0439453125

El valor de rr1 es 23040 y el de rr2 es 16808. A partir de dichos valores, queremos obtener otro valor en formato coma flotante simple (32 bits) (IEEE-754 Floating Point).
Para obtener el valor de la humedad en dicho formato, tenemos que realizar los siguientes pasos:
1.- Construir, a partir de los dos números decimales obtenidos, un número binario de 32 bits. Para ello pasamos cada número decimal obtenido a su valor binario, y le añadimos un 0 al inicio, para tener 8 bits en cada parte. Este paso lo realizamos con las líneas de código:

bin_number = '0' + str(np.base_repr(rr2.registers[0], base=2)) \
                         + '0' + str(np.base_repr(rr1.registers[0], base=2))

El resultado es el siguiente string que representa un número binario de 32 bits:
01000001101010000101101000000000

2. Lo siguiente es pasar el string a un entero, pasándole como argumento la base que queremos utilizar, en este caso, base 2.

f = int(bin_number, 2)

3. Utilizar la librería struck de python para poder poder convertir los tipos de datos obtenidos en el formato requerido:

struct.unpack('f', struct.pack('I', f))[0]

Con lo que obtenemos un valor para la temperatura de 21.04 ºC

En el este conversor online podemos comprobar que el resultado es correcto:

FloatConverter/IEEE754.html

Los datos se van añadiendo en una lista, la cual que se pasa como argumento al final del script a la función insertQuery, que se encarga de insertar dichos valores en una tabla de la base de datos mySQL.

Este script podemos automatizarlo utilizando cron, de manera que se ejecute, por ejemplo cada 15 minutos.

Para finalizar, podemos hacernos una función sencilla para la lectura de algunos datos de temperatura guardados en mySQL, y representarlos en un gráfico:

import mysql.connector
import pandas as pd
import matplotlib.pyplot as plt

try:
    connection=mysql.connector.connect(host='yourhost_maybe_localhost',
                                        database='nameofyourdatabase',
                                        user='youruser',
                                        password='yourpassword')
    cursor=connection.cursor()

    sql_query="""SELECT temperatura, timestamp FROM th_probe"""

    cursor.execute(sql_query)
    result=cursor.fetchall()

    header=['temperatura, timestamp']
    data=pd.Dataframe(result, columns=header)

    data.plot(kind='line', x='timestamp', y='temperatura', color='blue')
    plt.show()

except mysql.connector.Error as error:
    print("La conexion con la base de datos ha fallado {}".format(error))


Puedes descargarte el código de mi 

16 Comments

  • Ricardo

    12 junio, 2019 at 17:10

    Gracias por compartir tu info, estoy tratando de correr tu ejemplo del Post 17 y me da un error, que es “from pymodbus.client.sync import ModbusSerialClient as ModbusClient”.

    Puedes indicar como resolver este problema ?..

    Agradecido por tu tiempo de antemano.

    Saludos y gracias de nuevo

    Responder
  • Tikey Rivas

    19 septiembre, 2019 at 17:00

    Hola, soy nuevo en el mundo de Python y RTU Modbus, estoy buscando ayuda para poder comunicarme con un VDF.
    los primeros paso con el código los di usando el LABJACK y aunque me falta entender varias cosas pude encontrar unos totorales paso a paso para entender que pasa en cada uno de los comandos e ir armando un programa simple que me permite dependiendo de la lectura de las entradas digitales parar el código activar un contactor o leer la señal analógica.

    Pero ahora regresando al VDF estoy usando un adaptador a USB

    se que con este comando ModbusClient declaro a una variable que será como mi dispositivo el cual contara con todas las características para lectura y escritura

    en mi caso use este :

    c = ModbusClient(method=’rtu’, port=’/dev/tty.usbserial-AH05KHGO’, timeout=1, stopbits = 1, bytesize = 8, parity=’N’, baudrate= 9600)
    c.connect()

    la consola no me genera ningún tipo de error pero luego al intentar usar

    read_holding_registers

    me da errores.

    adicional al las dudas que tengo con el uso de la librería pymodbus también tengo dudas de las direcciones del VDF ya que el fabricante me facilito lo que seria el mapa de direcciones de memoria que pueden ser leídas y escritas y para que es cada una pero no me indica cuales son holding ni nada de eso, y en este aspecto quede en las nubes ya que no cuento con alguien que me pueda ayudar.

    tendrán un manual paso a paso para realizar la configuración y comunicación y expliquen para que es cada parámetro, incluso en ingles me ayudaría mucho (soluciono con el traductor)

    Responder
    • Tikey Rivas

      24 septiembre, 2019 at 00:20

      After spending a good time analyzing the instructions I was able to perform a VDF reading without indicating errors but I am not able to interpret the response or the reading I am doing poorly, since the memories I am reading are made up of several data
      example memory 1000H contains 8 data from 0001H to 0008H

      But the problem is that the code tells me
      “ReadRegisterResponse (1)”

      using the command
      “(c.read_holding_registers (address = 0x1000, starting_address = 0x0001, count = 0x0001, quantity = 0x1000, unit = 0x01)”

      for the VDF instructions stop and start and had no problems using

      “c.write_register (address = 0x1000, value = 1, unit = 0x01)”

      Responder
      • Tikey Rivas

        24 septiembre, 2019 at 12:01

        Gracias por la ayuda, el problema es más simple de lo que pensaba, todo es debido a un mal uso del las instrucciones o, a la falta de ellas.

        Como no encontré un ejemplo con este tipo de lectura no entendía bien como debía realizarlo pero tendría que ser algo de este tipo para realizar una lectura de memoria.

        DATO = client.read_holding_registers(value, address, unit=0x01)

        donde “valué” es una vector vacío
        “address” es la direction de memoria a leer
        “unit ” el dispositivo en el modbus a leer

        esto nos responde con “ReadRegisterResponse (1)”

        Lo que indica que se pudo leer correctamente un registro

        para hacer uso del dato obtenido se implementa

        DATO.registers[0]

        con “.registers[0]” se obtiene el dato guardado en la clase read_holding_registers de nombre “DATO”

        en esto ya logramos leer las memorias de un VDF y solo resta con una lista identificar a qué hace referencia cada uno de los datos que podría arrojar esta lectura.

        Código USADO para leer la moría “0x1001” de un VDF

        Direccion = 0x1001
        monitor = c.read_holding_registers(address=Direccion,starting_address=0x0001,count = 0x0001, unit=0x01)
        if not monitor.isError():
        for x in range(0,len(monitor.registers)):
        y = (monitor.registers[x])
        print (“Estado del VDF : “,end=””)
        if Direccion == 0x1001:
        if y == 1:
        print (“Forward running”)
        elif y == 2:
        print (“Reverserunning”)
        elif y == 3:
        print (“Standby”)
        elif y == 4:
        print (“Fault”)

        time.sleep(0.03)

        else:
        print (” “)
        print (“ERROR en Lectura : “,monitor.isError)

        Responder
  • Leonardo

    22 octubre, 2019 at 01:03

    Hola, como va?
    Como puedo hacer para leer datos de un registrador de energia trifasico marca circutor o schneider?. Ademas me interesaria poder registrar en un grafico los valores de Tension, corriente, etc (todos los parametros que arroja el registrador). Y luego poder graficas o disponer de los datos en una tabla.
    Tengo el conversos RS485 to USB
    Cordiales saludos
    Atte.-
    leonardo.-

    Responder
  • Tomas Sandoval

    3 junio, 2021 at 16:23

    Hola instale pymodbus y funciona bien, el problema que tengo es que me lanza un error de missingImports el
    “from stepTHconf import step_th”.
    Estoy programando en windows por ahora, sera ese el error?

    Responder
  • Carlos Nicolas Petry Fernandez

    6 junio, 2021 at 13:53

    Hola. Buen día. Se ve interesante tu explicación. Primero gracias por tu tiempo y segundo felicitaciones porque está muy claro y ordenado.
    Tengo una consulta que quizás me puedas orientar. Cuando hablamos de maestro esclavo, para el caso de modbus rtu. El maestro sería nuestra PC, PLC, raspberry o lo que esté ejecutando el programa que colocas de ejemplo. Y el esclavo, sería aquel instrumento de medición que tiene la información. Es correcto? Ahora bien, em caso de que quiera realizar ambas tareas se puede? O sea, por un lado, leer datos de diversos instrumentos. Pero por otro, que otros instrumentos, SCADA, o PLCs puedan leer mi raspberry…. gracias por tu tiempo. Saludos!

    Responder
    • koldo

      4 julio, 2021 at 11:36

      Buenas Carlos!, gracias por tu comentario.
      Sobre la arquitectura maestro-esclavo, es correcto.
      Sobre la segunda pregunta, si, es posible. Se puede un construir un sistema en la que otras aplicaciones puedan obtener información de tu raspberry. Por ejemplo utilizando protocolo OPC.

      Responder
  • Eduardo Ochoa

    9 marzo, 2022 at 21:15

    Felicitaciones por el conenido del artículo! Es necesario usar Ubuntu o un OS linux? O se podría realizar desde Windows? Espero puedas ver mi comentario crack!

    Responder
  • Carla

    17 octubre, 2022 at 14:24

    Hola! Muy buenas. Tu artículo me ha servido mucho para una parte de mi TFM, que era realizar consultas de un dispositivo con Modbus TCP. La otra parte te quería preguntar:
    ¿Como puedo leer los datos de 2 analizadores de redes, de circutor, que están conectados vía RTU, a un gestor energético EDS que lleva un Scada embebido y web server?
    Me explico, quisiera conectarme vía IP( la del eds, el cual tiene puerto 80) ya que este me sirve de gateway hacia los analizadores de redes, con periféricos 10 y 11.
    Un saludo muy grande!

    Responder

Deja una respuesta

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.