Ir al contenido

Introducción a Modbus y Raspberry PLC. Parte 3

Escribe y lee un coil por Modbus RTU en Python con pymodbus 3.16
21 de febrero de 2024 por
Introducción a Modbus y Raspberry PLC. Parte 3
Boot & Work Corp. S.L, Bernat Brunet Pedra

Guía completa: Modbus RTU y TCP con Python y Node-RED

Para escribir y leer un coil por Modbus RTU desde un Raspberry PLC en Python, instala pymodbus en un entorno virtual, abre el puerto RS-485 con ModbusSerialClient(port, framer=FramerType.RTU, ...) y llama a write_coil(address, value, device_id=...) (código de función 05) y a read_coils(address, count=1, device_id=...) (código de función 01). Los ejemplos de este post se han probado con pymodbus 3.16.1.

Esta es la parte 3 de la serie Modbus y Raspberry PLC. Las partes 1 y 2 usan Node-RED, y esta parte y la parte 4 usan Python. Todos los detalles, como Modbus TCP, timeouts, direccionamiento y ajustes de Node-RED, están en la guía de Modbus para PLC con Raspberry Pi.

ParteProtocoloHerramienta
Parte 1Modbus RTUNode-RED
Parte 2Modbus TCPNode-RED
Parte 3 (este post)Modbus RTUPython
Parte 4Modbus TCPPython

Qué necesitas

  • Un Raspberry PLC. Es el maestro Modbus RTU.
  • Un esclavo Modbus RTU: cualquier equipo con RS-485, u otro PLC de Industrial Shields que trabaje como esclavo, por ejemplo un PLC basado en Arduino o ESP32 como en Modbus RTU con PLC basados en Arduino y ESP32.
  • Cableado RS-485: A+ con A+ y B- con B-, con terminación en los extremos del bus.
  • Los parámetros serie del esclavo: dirección (device ID), velocidad, paridad y bits de parada. Este post usa el device ID 31 a 38400 bit/s, 8N1. Cámbialos para que coincidan con tu esclavo.

Instala pymodbus

Instala pymodbus con pip en un entorno virtual y fija la versión. Raspberry Pi OS Bookworm y posteriores bloquean pip install fuera de un entorno virtual, y la versión que instala apt depende de la versión del sistema. pymodbus cambia su API entre versiones, así que la versión decide si un ejemplo funciona.

python3 -m venv ~/modbus-venv
source ~/modbus-venv/bin/activate
pip install "pymodbus[serial]==3.16.1"
ls /dev/ttySC*

El último comando lista los puertos serie. En el Raspberry PLC v3, y en el v4 sin expansión, el RS-485 es /dev/ttySC0 y /dev/ttySC1. En el v6, y en el v4 con expansión, es /dev/ttySC2 y /dev/ttySC3. Este post usa /dev/ttySC0.

Escribe un coil con Python (FC05)

Guarda este código como rtu_write_coil.py:

from pymodbus.client import ModbusSerialClient
from pymodbus.exceptions import ModbusException
from pymodbus.framer import FramerType

PORT = "/dev/ttySC0"  # RS-485 port, check the name for your PLC version
DEVICE_ID = 31        # Modbus address of the slave (1 to 247, never 0)
COIL = 0              # coil address (coil 00001 in the device manual is address 0)

client = ModbusSerialClient(
    port=PORT,
    framer=FramerType.RTU,
    baudrate=38400,
    bytesize=8,
    parity="N",
    stopbits=1,
    timeout=1,
)
if not client.connect():
    raise SystemExit(f"Cannot open {PORT}")

try:
    rr = client.write_coil(COIL, True, device_id=DEVICE_ID)  # FC05
    if rr.isError():
        print("Exception response:", rr)
    else:
        print(f"Coil {COIL} set to ON")
except ModbusException as exc:
    print("No valid answer:", exc)  # timeout, CRC error or wrong device ID
finally:
    client.close()
  1. Importaciones. ModbusSerialClient es el maestro RTU, FramerType selecciona la trama RTU y ModbusException captura los casos en que el esclavo no responde.
  2. Puerto y esclavo. DEVICE_ID es la dirección Modbus del esclavo. La dirección 0 es broadcast: los esclavos no le responden.
  3. Cliente. La velocidad, los bits de datos, la paridad y los bits de parada deben ser los mismos que en el esclavo. timeout=1 espera un segundo cada respuesta.
  4. Escritura. write_coil(COIL, True, device_id=DEVICE_ID) envía el código de función 05. Usa False para apagar el coil.
  5. Dos tipos de error. rr.isError() es una respuesta de excepción: el esclavo ha respondido pero rechaza la petición, por ejemplo porque la dirección no existe. ModbusException significa que no ha llegado ninguna respuesta válida.

Ejecútalo con el entorno virtual activo:

source ~/modbus-venv/bin/activate
python rtu_write_coil.py
Coil 0 set to ON
Terminal de un Raspberry PLC: python rtu.py se ejecuta dos veces y escribe el valor 0 y después 1 en la dirección de coil 0
Salida de la primera versión del script en un Raspberry PLC: escribe 0 y después 1 en el coil 0.

Escribe y lee un coil en un bucle (FC05 y FC01)

Esta versión cambia el coil cada segundo y lo vuelve a leer para comprobar el valor:

import time

from pymodbus.client import ModbusSerialClient
from pymodbus.exceptions import ModbusException
from pymodbus.framer import FramerType

PORT = "/dev/ttySC0"  # RS-485 port, check the name for your PLC version
DEVICE_ID = 31
COIL = 0

client = ModbusSerialClient(
    port=PORT,
    framer=FramerType.RTU,
    baudrate=38400,
    bytesize=8,
    parity="N",
    stopbits=1,
    timeout=1,
)
if not client.connect():
    raise SystemExit(f"Cannot open {PORT}")

state = True
try:
    while True:
        try:
            client.write_coil(COIL, state, device_id=DEVICE_ID)        # FC05
            rr = client.read_coils(COIL, count=1, device_id=DEVICE_ID)  # FC01
            if rr.isError():
                print("Exception response:", rr)
            else:
                print(f"Wrote {state}, read back {rr.bits[0]}")
        except ModbusException as exc:
            print("No valid answer:", exc)
        state = not state
        time.sleep(1)
except KeyboardInterrupt:
    pass
finally:
    client.close()
  • read_coils(COIL, count=1, device_id=DEVICE_ID) envía el código de función 01. La respuesta agrupa los coils en bytes, así que rr.bits tiene 8 valores aunque leas un solo coil. El primero, rr.bits[0], es el coil que has pedido.
  • El cliente abre el puerto una vez y lo mantiene abierto. Cada ciclo captura sus propios errores, así que una respuesta que falta no detiene el bucle.
  • Pulsa Ctrl+C para parar. El puerto se cierra en finally.

Salida de nuestra prueba:

Wrote True, read back True
Wrote False, read back False
Wrote True, read back True
Wrote False, read back False
Terminal de un Raspberry PLC: el bucle de escritura y lectura muestra una primera escritura fallida y después lee True, False y True del coil 0
Captura de la primera versión en un Raspberry PLC: la primera escritura falla y después el coil alterna entre True y False.

Errores con ejemplos antiguos

La primera versión de este post usaba ModbusSerialClient(method='rtu', ...) y el device ID como argumento posicional. La ejecutamos contra un esclavo simulado con varias versiones de pymodbus: funciona con la 3.0.2 y la 3.6.9, y falla con la 3.7 y posteriores.

ErrorCausaSolución
unexpected keyword argument 'method'pymodbus 3.7 y posteriores seleccionan la trama con framerframer=FramerType.RTU
unexpected keyword argument 'slave'pymodbus 3.10 y posteriores lo llaman device_iddevice_id=31
cannot import name 'ModbusSerialClient' from 'pymodbus.client'pymodbus 2.xInstala pymodbus 3 en un entorno virtual
No response receivedEl esclavo no respondeDevice ID, velocidad, paridad, A y B sin cruzar, terminación
Respuesta de excepción con código 02La dirección del coil no existe en el esclavoLa tabla de coils del manual del esclavo

Qué hemos probado. Python 3.13 y pymodbus 3.16.1 en Linux, con un esclavo Modbus RTU simulado (device ID 31, 38400 8N1) en un enlace serie virtual. Los dos scripts se ejecutaron y dieron la salida que se muestra arriba. El código original se ejecutó de la misma forma con pymodbus 2.5.3, 3.0.2, 3.6.9, 3.7.4, 3.8.6 y 3.16.1. No hemos ejecutado esta actualización en un Raspberry PLC real: las dos capturas son de la primera versión del post.

Modbus RTU en un Raspberry PLC en una planta real. En el caso de estudio de la planta de biogás, un Raspberry PLC 58 controla cuatro secciones del proceso, y los contadores con Modbus RTU van en uno de sus dos puertos RS-485.

Lee el caso de la planta de biogásAbre la guía completa de Modbus

Preguntas frecuentes sobre Modbus RTU con Python

El manual dice coil 00001. ¿Qué dirección uso?

La dirección 0. pymodbus usa la dirección de la trama, que empieza en 0, y muchos manuales numeran los coils desde 1.

¿Por qué read_coils devuelve 8 valores?

Porque la respuesta lleva los coils en bytes enteros. pymodbus devuelve todos los bits de esos bytes. Usa solo los primeros count valores.

¿Puedo instalar pymodbus con apt?

Puedes, pero obtienes la versión de tu sistema, que puede tener una API distinta de la de los ejemplos. Un entorno virtual con una versión fija da el mismo resultado en todos los PLC.

¿Puedo usar el device ID 0?

Solo para escrituras broadcast. Todos los esclavos las ejecutan y ninguno responde, así que no puedes leer con el device ID 0.

¿Cómo hago lo mismo por Ethernet?

Con ModbusTcpClient y la dirección IP del esclavo. Consulta la parte 4: Modbus TCP y Python.

Buscar en nuestro blog

Introducción a Modbus y Raspberry PLC. Parte 3
Boot & Work Corp. S.L, Bernat Brunet Pedra 21 de febrero de 2024
Compartir esta publicación

¿Estás buscando tu Controlador Lógico Programable ideal?

Echa un vistazo a esta comparativa de producto de varios controladores industriales basados en Arduino.

Comparamos entradas, salidas, comunicaciones y otras especificaciones con las de los equipos de otras marcas destacadas.

Comparativa de PLCs