Introduction
Communicating and controlling an Arduino based PLC can be done in several ways, such as with Modbus, MQTT, Ethernet, RS-232 and RS-485, ... But surely one of the simplest ones is using the Serial port. Communicating through Serial can also be done with different tools, for example Arduino IDE, C or C++ scripts, python scripts and more.
In this post we will control the PLC using Serial port communication and Node-RED Dashboard acting as HMI. To connect both interface and PLC a python script will be used, so let's get into it.
Requirements
Node-RED flow
To demonstrate the functionality of the project we will use 5 digital outputs. First, open the Node-RED environment with:
$ node-red-start
And go to http://127.0.0.1:1800/.
Start by dragging 5 switch nodes and add them to the group you prefer. Change the topic to the number you want to assign the output pin of the PLC to:
As it can be seen, output Q0_0 is in the group "Outputs" from tab "Python", and its topic is 0, because it is referred to output number 0. In the other nodes, the only thing that changes is the name of the node (Q0_0, Q0_1, ...) and the topic value, from 0 to 4.
After having configured the switch nodes, connect all them to a function node, whose job will be to create a string based on the state and the topic value of the switch that has been clicked.
The code inside the function node is going to be the following. The code will be directly related to the python code, so changing any of them would derivate on properly adjusting the other one.
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;
The function node will look at the current state of the switch node and modify the array named "pins" in order to keep a log of all the output states. Then, it is converted to string, which will be later appended to an exec node.
An output example of this node would be: "H0,,,," or ",H1,L2,H3,H4".
Now connect it to an exec node. Configure it like so:
As the name suggests, this node will run the command just like it was a terminal window. So, the node will execute the python script to send the desired instruction to the PLC. The path to the python script needs to be changed to whatever path leads to your python script.
Complete flow code ready to import:
[{"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}]
Python script
The first thing to do is to import serial, sys and time libraries.
import serial
import sys
import time
Then, define a function with 2 parameters. This function will send bytes to the PLC so it can read them and act correspondingly.
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()
In the example, the PLC has the port /dev/ttyACM0, but change it to whatever port you are using. As it can be seen in the code, serial communication is first established with the PLC and then, 2 bytes are sent. First, the value that the pin will get (HIGH or LOW), and then the number of the pin itself.
This is done for every switch that has been clicked, because when serial port is opened with serial.Serial(), the PLC gets reseted every time the script gets executed.
At last but not least, add the line that will call the function with the desired parameters:
outputDo(sys.argv[1])
For example, to turn on output Q0_2, 'H2' is sent to the PLC ('H' for HIGH and '2' for pin number 2). Notice how before sending any byte the program is stopped by 2 seconds, so the serial port can initiate correctly.
POSSIBLE DOWNGRADE
As seen, when serial port is opened with serial.Serial(), the PLC gets reseted and nothing can be done to avoid this performance. This is undesirable for some projects, because the PLC is not able to remember the last state of the outputs. To solve this problem, in this post the python scripts receives as parameters all the important output values so it keeps track of what has happened before.
One solution to the problem is to open a terminal window and start a python console there with
$ python3
There, import the serial library and start a serial communication just like in the python script:
>>> import serial
>>> ser = serial.Serial('/dev/ttyACM0')
After doing so, the PLC should not restart whenever the python script is executed.
Arduino code
Finally, the Arduino code. This code will read from the Serial port and will turn on/ off the output pins that the python script wants to switch.
Like in the python script, baudrate from Serial port will be 9600.
void setup() {
Serial.begin(9600UL);
}
In the loop function, the Arduino based PLC will wait for a total of 2 bytes received in the Serial bus, and then the PLC will check if the data received is correct, and then proceed to turn on/ off the corresponding pin.
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);
}
}
}
For any other bytes that do not match any of the if clauses, Q0_5 output will turn on.
Conclusions
In this post you have learnt another possibility of interaction between User and Arduino based PLC, this time using Node-RED and a python script to connect them. In the following image you can see a summary of all the steps the instruction has to go through and the changes it gets before output Q0_0 from the PLC gets a HIGH level.
This opens a huge window of possibilities to work with Arduino based PLCs, as they are very versatile and easy to control with only a few lines of code.
Controlling an Arduino based PLC using a python script and Node-RED