Introducción
La comunicación y el control de un PLC basado en Arduino pueden hacerse de varias formas, por ejemplo con Modbus, MQTT, Ethernet, RS-232 y RS-485, ... Pero seguramente una de las más sencillas es usar el puerto Serial. La comunicación por Serial también puede hacerse con distintas herramientas, por ejemplo Arduino IDE, scripts en C o C++, scripts en python y más.
En este post controlaremos el PLC mediante comunicación por puerto Serial y Node-RED Dashboard como HMI. Para conectar la interfaz y el PLC se utilizará un script en python. Vamos a ello.
Requisitos
Flujo de Node-RED
Para demostrar el funcionamiento del proyecto utilizaremos 5 salidas digitales. Primero, abre el entorno Node-RED con:
$ node-red-start
Y accede a http://127.0.0.1:1800/.
Empieza arrastrando 5 nodos switch y añádelos al grupo que prefieras. Cambia el topic por el número al que quieras asignar el pin de salida del PLC:
Como se puede ver, la salida Q0_0 está en el grupo "Outputs" de la pestaña "Python", y su topic es 0, porque hace referencia a la salida número 0. En el resto de nodos, lo único que cambia es el nombre del nodo (Q0_0, Q0_1, ...) y el valor del topic, de 0 a 4.
Una vez configurados los nodos switch, conéctalos todos a un nodo function, cuya tarea será crear una cadena de texto a partir del estado y del valor del topic del switch que se ha pulsado.
El código del nodo function será el siguiente. Este código está directamente relacionado con el código python, por lo que si cambias uno de los dos tendrás que ajustar también el otro.
var pins = context.get('pins') || [];
if (msg.payload) {
pins[parseInt(msg.topic)] = "H" + msg.topic;
}
else {
pins[parseInt(msg.topic)] = "L" + msg.topic;
}
msg.payload = pins.toString();
context.set('pins', pins);
return msg;
El nodo function consultará el estado actual del nodo switch y modificará el array llamado "pins" para llevar un registro de todos los estados de las salidas. Después se convierte en una cadena de texto, que más adelante se añadirá a un nodo exec.
Un ejemplo de salida de este nodo sería: "H0,,,," o ",H1,L2,H3,H4".
Ahora conéctalo a un nodo exec. Configúralo así:
Como su nombre indica, este nodo ejecutará el comando igual que si fuera una ventana de terminal. Así, el nodo ejecutará el script python para enviar la instrucción deseada al PLC. La ruta del script python debe cambiarse por la ruta que lleve a tu script python.
Código completo del flujo, listo para importar:
[{"id":"6e00259da3979f87","type":"exec","z":"b4510ccb991e223f","command":"python3 ~/path/to/python/script.py","addpay":"payload","append":"","useSpawn":"false","timer":"","winHide":false,"oldrc":false,"name":"","x":480,"y":180,"wires":[[],[],[]]},{"id":"720ccb1d0aed9827","type":"function","z":"b4510ccb991e223f","name":"","func":"var pins = context.get('pins') || [];\n\nif (msg.payload) {\n pins[parseInt(msg.topic)] = \"H\" + msg.topic;\n}\nelse {\n pins[parseInt(msg.topic)] = \"L\" + msg.topic;\n}\nmsg.payload = pins.toString();\n\ncontext.set('pins', pins);\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":260,"y":180,"wires":[["6e00259da3979f87"]]},{"id":"28a036645c45e4e7","type":"ui_switch","z":"b4510ccb991e223f","name":"","label":"Q0_0","tooltip":"","group":"aa589798ec116538","order":8,"width":0,"height":0,"passthru":true,"decouple":"false","topic":"0","topicType":"str","style":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","animate":false,"className":"","x":90,"y":100,"wires":[["720ccb1d0aed9827"]]},{"id":"1864650bc56ed4b0","type":"ui_switch","z":"b4510ccb991e223f","name":"","label":"Q0_1","tooltip":"","group":"aa589798ec116538","order":8,"width":0,"height":0,"passthru":true,"decouple":"false","topic":"1","topicType":"str","style":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","animate":false,"className":"","x":90,"y":140,"wires":[["720ccb1d0aed9827"]]},{"id":"4bca242485fff5c3","type":"ui_switch","z":"b4510ccb991e223f","name":"","label":"Q0_2","tooltip":"","group":"aa589798ec116538","order":8,"width":0,"height":0,"passthru":true,"decouple":"false","topic":"2","topicType":"str","style":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","animate":false,"className":"","x":90,"y":180,"wires":[["720ccb1d0aed9827"]]},{"id":"e7f289ea756bacc0","type":"ui_switch","z":"b4510ccb991e223f","name":"","label":"Q0_3","tooltip":"","group":"aa589798ec116538","order":8,"width":0,"height":0,"passthru":true,"decouple":"false","topic":"3","topicType":"str","style":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","animate":false,"className":"","x":90,"y":220,"wires":[["720ccb1d0aed9827"]]},{"id":"53fa980e3357775c","type":"ui_switch","z":"b4510ccb991e223f","name":"","label":"Q0_4","tooltip":"","group":"aa589798ec116538","order":8,"width":0,"height":0,"passthru":true,"decouple":"false","topic":"4","topicType":"str","style":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","animate":false,"className":"","x":90,"y":260,"wires":[["720ccb1d0aed9827"]]},{"id":"aa589798ec116538","type":"ui_group","name":"Outputs","tab":"fef0aab58c820af7","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"fef0aab58c820af7","type":"ui_tab","name":"Python","icon":"dashboard","disabled":false,"hidden":false}]
Script de Python
Lo primero es importar las librerías serial, sys y time.
import serial
import sys
import time
A continuación, define una función. Esta función enviará bytes al PLC para que los lea y actúe en consecuencia.
def outputDo(pins):
ser = serial.Serial('/dev/ttyACM0', 9600)
time.sleep(2)
all_pins = pins.split(",")
for i in range(len(all_pins)):
if (all_pins[i] != "")):
ser.write(all_pins[i][0].encode())
ser.write(all_pins[i][1].encode())
ser.close()
En el ejemplo, el PLC está en el puerto /dev/ttyACM0, pero cámbialo por el puerto que estés usando. Como se puede ver en el código, primero se establece la comunicación serie con el PLC y después se envían 2 bytes: primero, el valor que tomará el pin (HIGH o LOW) y, después, el número del propio pin.
Esto se hace por cada switch pulsado, porque cuando el puerto serie se abre con serial.Serial(), el PLC se reinicia cada vez que se ejecuta el script.
Por último, añade la línea que llamará a la función con los parámetros deseados:
outputDo(sys.argv[1])
Por ejemplo, para activar la salida Q0_2 se envía 'H2' al PLC ('H' de HIGH y '2' de número de pin 2). Fíjate en que, antes de enviar ningún byte, el programa se detiene 2 segundos para que el puerto serie pueda iniciarse correctamente.
POSIBLE INCONVENIENTE
Como se ha visto, cuando el puerto serie se abre con serial.Serial(), el PLC se reinicia y no se puede hacer nada para evitarlo. Esto es indeseable en algunos proyectos, porque el PLC no es capaz de recordar el último estado de las salidas. Para resolver este problema, en este post el script python recibe como parámetros todos los valores importantes de las salidas, de modo que lleva el control de lo que ha ocurrido antes.
Una solución al problema es abrir una ventana de terminal e iniciar allí una consola de python con
$ python3
Allí, importa la librería serial e inicia una comunicación serie igual que en el script python:
>>> import serial
>>> ser = serial.Serial('/dev/ttyACM0')
Después de hacerlo, el PLC no debería reiniciarse cada vez que se ejecute el script python.
Código Arduino
Por último, el código Arduino. Este código leerá del puerto Serial y activará o desactivará los pines de salida que el script python quiera conmutar.
Igual que en el script python, la velocidad en baudios del puerto Serial será 9600.
void setup() {
Serial.begin(9600UL);
}
En la función loop, el PLC basado en Arduino esperará a recibir un total de 2 bytes por el bus Serial, comprobará después si los datos recibidos son correctos y a continuación activará o desactivará el pin correspondiente.
void loop() {
while (Serial.available()) {
byte rx0 = Serial.read();
while (!Serial.available());
byte rx1 = Serial.read();
if (rx0 == 'H') {
if (rx1 == '0') digitalWrite(Q0_0, HIGH);
else if (rx1 == '1') digitalWrite(Q0_1, HIGH);
else if (rx1 == '2') digitalWrite(Q0_2, HIGH);
else if (rx1 == '3') digitalWrite(Q0_3, HIGH);
else if (rx1 == '4') digitalWrite(Q0_4, HIGH);
else digitalWrite(Q0_5, HIGH);
}
else if (rx0 == 'L') {
if (rx1 == '0') digitalWrite(Q0_0, LOW);
else if (rx1 == '1') digitalWrite(Q0_1, LOW);
else if (rx1 == '2') digitalWrite(Q0_2, LOW);
else if (rx1 == '3') digitalWrite(Q0_3, LOW);
else if (rx1 == '4') digitalWrite(Q0_4, LOW);
else digitalWrite(Q0_5, HIGH);
}
else {
digitalWrite(Q0_5, HIGH);
}
}
}
Para cualquier otro byte que no coincida con ninguna de las cláusulas if, se activará la salida Q0_5.
Conclusiones
En este post has aprendido otra posibilidad de interacción entre el usuario y un PLC basado en Arduino, esta vez usando Node-RED y un script python para conectarlos. En la siguiente imagen puedes ver un resumen de todos los pasos por los que pasa la instrucción y los cambios que sufre antes de que la salida Q0_0 del PLC pase a nivel HIGH.
Esto abre un amplio abanico de posibilidades para trabajar con PLC basados en Arduino, ya que son muy versátiles y fáciles de controlar con solo unas pocas líneas de código.