Node-RED è un ambiente di sviluppo visuale open-source progettato per la creazione di flussi di lavoro basati su nodi, comunemente utilizzato per l’automazione e l’integrazione di sistemi IoT (Internet delle cose). Un editor basato su browser permette di interconnettere tra di loro oggetti (Nodi), i nodi sono i mattoncini di base che costituiscono un flusso di lavoro. Ogni nodo esegue una specifica funzione ed esistono moltissimi nodi pronti per l’uso scaricabili direttamente dal web.
Node-RED disponibile anche sul Gateway industriale LoRaWAN Milesight, in questo articolo con il programma dimostrativo Ptp207, vedremo come interfacciare un programma Node-RED con un programma PLC sviluppato con LogicLab.
In Node-RED è stata sviluppata una semplice dashboard con la visualizzazione di 2 LED e la gestione di 2 tasti. La variazione di stato delle variabili sullo SlimLine attiva i LED, la variazione di stato dei tasti attiva variabili nello SlimLine. E’ possibile eseguire il download del programma ed adattarlo alle proprie esigenze.

Descrizione programma di esempio
Per connettere il gateway LoRaWAN ai nostri sistemi programmabili SlimLine possiamo utilizzare diversi metodi, ad esempio il protocollo Modbus, ma un modo più semplice è utilizzare una semplice connessione UDP o TCP, vediamo un esempio che utilizza il nodo tcp request.

Nel flow il nodo tcp request, esegue una connessione in TCP con lo SlimLine ed invia un messaggio JSON con lo stato degli switches della dashboard per il comando delle uscite. In risposta viene ricevuto un messaggio JSON con lo stato degli ingressi che viene decodificato ed appoggiato sui LED della dashboard.
Node-RED flow
[{"id":"9a87f518.8e4c78","type":"tab","label":"SlimLine","disabled":false,"info":""},{"id":"407dbcc8.951b14","type":"comment","z":"9a87f518.8e4c78","name":"Set logic outputs","info":"","x":480,"y":40,"wires":[]},{"id":"70a9f679.7c4d38","type":"function","z":"9a87f518.8e4c78","name":"Set flow variables","func":"flow.set(msg.topic, msg.payload); //Set flow variable\n","outputs":0,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":630,"y":80,"wires":[]},{"id":"ef3e69bf.e53878","type":"ui_switch","z":"9a87f518.8e4c78","name":"Do00","label":"Do00","tooltip":"","group":"7793e86a.8ca5d8","order":3,"width":0,"height":0,"passthru":true,"decouple":"false","topic":"Do00","topicType":"str","style":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","animate":true,"className":"","x":450,"y":80,"wires":[["70a9f679.7c4d38"]]},{"id":"e6cfa300.ea842","type":"ui_switch","z":"9a87f518.8e4c78","name":"Do01","label":"Do01","tooltip":"","group":"7793e86a.8ca5d8","order":4,"width":0,"height":0,"passthru":true,"decouple":"false","topic":"Do01","topicType":"str","style":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","animate":true,"className":"","x":450,"y":140,"wires":[["70a9f679.7c4d38"]]},{"id":"1233c636.a225da","type":"comment","z":"9a87f518.8e4c78","name":"Show logic inputs","info":"","x":800,"y":200,"wires":[]},{"id":"c7116466.9c1208","type":"function","z":"9a87f518.8e4c78","name":"Initializing","func":"flow.set('Do00', false); //Set flow variable\nflow.set('Do01', false); //Set flow variable\n","outputs":0,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":260,"y":80,"wires":[]},{"id":"669d70c0.97f0c","type":"inject","z":"9a87f518.8e4c78","name":"Init pulse","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"true","payloadType":"bool","x":100,"y":80,"wires":[["c7116466.9c1208"]]},{"id":"7fe96950.b214a8","type":"comment","z":"9a87f518.8e4c78","name":"Flow initializing","info":"Eseguo set uscite logiche","x":100,"y":40,"wires":[]},{"id":"4e674419.00a14c","type":"inject","z":"9a87f518.8e4c78","name":"Clock","props":[{"p":"payload"}],"repeat":"5","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":90,"y":260,"wires":[["767cba89.c7ba74"]]},{"id":"4f015926.a808a8","type":"debug","z":"9a87f518.8e4c78","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":450,"y":320,"wires":[]},{"id":"767cba89.c7ba74","type":"function","z":"9a87f518.8e4c78","name":"JSON Encode","func":"msg.payload='{\"Do00\":'+flow.get('Do00')+',\"Do01\":'+flow.get('Do01')+'}';\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":260,"y":260,"wires":[["16d44eb9.b5b5e1","4f015926.a808a8"]]},{"id":"16d44eb9.b5b5e1","type":"tcp request","z":"9a87f518.8e4c78","name":"Connection","server":"192.168.0.181","port":"3000","out":"time","ret":"buffer","splitc":"1000","newline":"","trim":false,"tls":"","x":470,"y":260,"wires":[["55c19af5.3d3204","186f8eb7.dc8731"]]},{"id":"55c19af5.3d3204","type":"function","z":"9a87f518.8e4c78","name":"","func":"// Convert buffer data to string.\n\nvar RxD=Buffer.from(msg.payload).toString();\nnode.warn(RxD);\n\n// Parse the JSON data.\n\nvar JData = JSON.parse(RxD);\n\nmsg.payload={\"Di00\":JData.Di00, \"Di01\":JData.Di01};\n\nvar msg1={payload:JData.Di00};\nvar msg2={payload:JData.Di01};\nreturn [msg1, msg2];\n","outputs":2,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":680,"y":260,"wires":[["f10e05ca902e8ac3"],["d010fb49f12c0ef4"]]},{"id":"186f8eb7.dc8731","type":"debug","z":"9a87f518.8e4c78","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":650,"y":300,"wires":[]},{"id":"f10e05ca902e8ac3","type":"ui_led","z":"9a87f518.8e4c78","order":1,"group":"7793e86a.8ca5d8","width":0,"height":0,"label":"Di00","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#808080","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"Di00","x":830,"y":240,"wires":[]},{"id":"d010fb49f12c0ef4","type":"ui_led","z":"9a87f518.8e4c78","order":2,"group":"7793e86a.8ca5d8","width":0,"height":0,"label":"Di01","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#808080","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"Di01","x":830,"y":280,"wires":[]},{"id":"8a68c199ecfbc557","type":"comment","z":"9a87f518.8e4c78","name":"SlimLine TCP/IP communication","info":"","x":450,"y":200,"wires":[]},{"id":"7793e86a.8ca5d8","type":"ui_group","name":"Logic I/O","tab":"b0e50db.8ccdbf","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"b0e50db.8ccdbf","type":"ui_tab","name":"Home","icon":"dashboard","disabled":false,"hidden":false}]
Il programma LogicLab istanzia un server TCP sulla porta 3000 ed attende la ricezione della stringa JSON con il comando delle uscite logiche. Copia lo stato delle uscite invertito sugli ingressi ed invia in risposta una stringa JSON con lo stato degli ingressi.
LogicLab (Ptp207, NodeRED)
PROGRAM NodeRED
VAR CONSTANT
DBSize : UDINT := 512; (* Rx data size (SysRMAlloc) *)
END_VAR
VAR
i : UDINT; (* Auxiliary variable *)
CaseNr : USINT; (* Program case *)
Fp : eFILEP; (* File pointer array *)
Do : ARRAY[0..1] OF BOOL; (* Digital outputs *)
Di : ARRAY[0..1] OF BOOL; (* Digital inputs *)
TCPServer : SysTCPServer; (* TCPServer management *)
CLI : CLIServer; (* Command line interface server *)
DBuf : @STRING; (* Data buffer (SysRMAlloc) *)
END_VAR
// *****************************************************************************
// PROGRAM "NodeRED"
// *****************************************************************************
// The program accepts a TCP connection, digital outputs is set according the
// json message received. Upon reception the status of logic inputs is sent
// encoded on a json message.
// -----------------------------------------------------------------------------
// -------------------------------------------------------------------------
// INITIALIZATION
// -------------------------------------------------------------------------
// First program execution loop initializations.
IF (SysFirstLoop) THEN
// TCPClient initialization.
TCPServer.FilesArr:=ADR(Fp); //Files array
TCPServer.LocalAdd:=ADR('0.0.0.0'); //Local address
TCPServer.LocalPort:=3000; //Local port
TCPServer.MaxConn:=1; //Accepted connections
TCPServer.FlushTm:=50; //Flush time (mS)
TCPServer.LifeTm:=30; //Life time (S)
TCPServer.RxSize:=256; //Rx buffer size
TCPServer.TxSize:=256; //Tx buffer size
// Command line interface initialization.
CLI.SpyOn:=FALSE; //Spy On
CLI.EchoOn:=FALSE; //Echo On
CLI.CBSize:=512; //Command buffer size
CLI.CBegin:=ADR('{'); //Command begin
CLI.CBLength:=Sysstrlen(CLI.CBegin); //Command begin length
CLI.CEnd:=ADR('}'); //Command end
CLI.CELength:=Sysstrlen(CLI.CEnd); //Command end length
CLI.CWTime:=T#100ms; //Character waiting
CLI.Timeout:=T#1s; //Timeout
END_IF;
// Manage the TCP server.
TCPServer(Enable:=TRUE); //TCPServer management
CLI(Fp:=Fp); //Command interface server
IF NOT(SysFIsOpen(Fp)) THEN CaseNr:=0; RETURN; END_IF;
// -------------------------------------------------------------------------
// PROGRAM SEQUENCIES
// -------------------------------------------------------------------------
// Manage the program sequencies.
CASE (CaseNr) OF
// ---------------------------------------------------------------------
// WAITING THE COMMAND
// ---------------------------------------------------------------------
// Waiting the command reception.
0:
CLI.Enable:=TRUE; //CLI server enable
IF (DBuf <> eNULL) THEN i:=SysRMFree(ADR(DBuf)); END_IF;
IF NOT(CLI.Done) THEN RETURN; END_IF;
i:=SysWrSpyData(SPY_ASCHEX, 0, 16#00000001, ADR('NodeRED:Rx'), CLI.CBuffer);
// Received JSON string is decoded.
// {"Do00":false,"Do01":true}
i:=JSONDecoder(CLI.CBuffer, ADR('Do00'), BOOL_TYPE, ADR(Do[0]), 1, 0);
i:=JSONDecoder(CLI.CBuffer, ADR('Do01'), BOOL_TYPE, ADR(Do[1]), 1, 0);
CLI.Enable:=FALSE; //CLI server enable
CaseNr:=CaseNr+1; //Program case
// ---------------------------------------------------------------------
// Alloc memory buffer and send the answer message.
// {"Di00":false,"Di01":true}
1:
IF NOT(SysRMAlloc(DBSize, ADR(DBuf))) THEN RETURN; END_IF;
i:=JSONEncoder(DBuf, DBSize, ADR('Di00'), BOOL_TYPE, ADR(Di[0]), 1);
i:=JSONEncoder(DBuf, DBSize, ADR('Di01'), BOOL_TYPE, ADR(Di[1]), 1);
i:=SysWrSpyData(SPY_ASCHEX, 0, 16#00000002, ADR('NodeRED:Tx'), DBuf);
i:=Sysfwrite(DBuf, 1, TO_INT(Sysstrlen(DBuf)), Fp);
CaseNr:=0; //Program case
END_CASE;;
// [End of file]