V tomto pokračování našeho seriálu si ukážeme jak s Quidy v prostředí Node-REDu komunikovat protokolem Spinel. Velkou výhodou Spinelu je možnost okamžitých notifikací o změně na vstupech. Okamžité notifikace mají hned několik výhod: (1) nemusíte programově "obíhat" vstupy na všech Quidech a zjišťovat jestli došlo ke změně nebo ne, (2) nemůže se stát, že nějakou změnu prošvihnete a (3) princip odesílání notifikací je stejný u všech Quid bez ohledu na komunikační rozhraní.
Seriál o Node-REDu |
1. čtení dat z teploměrů přes Ethernet a RS485 |
2. zápis dat do Google Tabulky |
3. automatizace s I/O moduly Quido - protokol Modbus |
4. I/O moduly Quido - protokol Spinel |
V příkladech je použito Quido RS 4/4, připojené pomocí sběrnice RS485 k Raspberry Pi přes USB převodník SB485L. Když budete příklady testovat s jiným Quidem, je možné, že se setkáte se změnami vyplývajícími z jiného počtu vstupů a výstupů.
Pomocí Menu > Manage Palette > Install a ikony Upload najděte u sebe na disku soubor s těmito nody pro komunikaci Spinelem (soubor *.tgz, velikost < 50 kB).
Instalace nodů pro Spinel do prostředí Node-RED
V příkladech níže se používá sériový port. Příklad nalezení a nastavení portu je na obrázcích:
Sériový port převodníku najdete kliknutím na ikonu lupy
Nastavení portu
Příklady je možné použít i v případě komunikace přes Ethernet. Jen je třeba místo nodů pro sériový port použít nody TCP (v případě TCP spojení) nebo UDP (pro UDP komunikaci).
Pokud chcete nahrát příklad do svého Node-RED, vložte následující kód pomocí Menu > Import > Clipboard.
[ { "id": "cb29d1e93beed148", "type": "inject", "z": "0432e84f736d7dc4", "name": "", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "", "payloadType": "date", "x": 320, "y": 480, "wires": [ [ "7668d4343dacbac3" ] ] }, { "id": "7668d4343dacbac3", "type": "function", "z": "0432e84f736d7dc4", "name": "Only one", "func": "msg.payload = {\n ADR: 0x31, // Default address of Quido\n INST: 0x20, // Instruction code\n DATA: [0x81] // Array with one element, so we control one output. The most significant bit is 1, the other bits represent the output number - here 1.\n}\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 480, "y": 480, "wires": [ [ "22dc1a5daf8f522b" ] ] }, { "id": "22dc1a5daf8f522b", "type": "Spinel Tx", "z": "0432e84f736d7dc4", "name": "", "x": 640, "y": 480, "wires": [ [ "3ac828118932cf8d" ] ] }, { "id": "f23b338089a531d9", "type": "Spinel Rx", "z": "0432e84f736d7dc4", "name": "", "format": "object", "x": 480, "y": 600, "wires": [ [ "5af2a43dd9086a8f" ] ] }, { "id": "6376affd1fa5e44e", "type": "debug", "z": "0432e84f736d7dc4", "name": "debug 5", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 800, "y": 600, "wires": [] }, { "id": "5af2a43dd9086a8f", "type": "function", "z": "0432e84f736d7dc4", "name": "function 2", "func": "if (msg.payload.ACK == 0x00) {\n node.status({ fill: \"green\", shape: \"dot\", text: \"ACK Ok\" });\n} else if (msg.payload.ACK <= 0x09) {\n node.status({ fill: \"red\", shape: \"dot\", text: \"ACK Error!\" });\n} else if (msg.payload.ACK <= 0x0F) {\n node.status({ fill: \"blue\", shape: \"dot\", text: \"ACK Auto message\" });\n}\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 640, "y": 600, "wires": [ [ "6376affd1fa5e44e" ] ] }, { "id": "3ac828118932cf8d", "type": "serial out", "z": "0432e84f736d7dc4", "name": "", "serial": "0c1b9e2080c09818", "x": 810, "y": 480, "wires": [] }, { "id": "3d6f5581db5a7adf", "type": "serial in", "z": "0432e84f736d7dc4", "name": "", "serial": "0c1b9e2080c09818", "x": 310, "y": 600, "wires": [ [ "f23b338089a531d9" ] ] }, { "id": "fef8cc39d2892210", "type": "comment", "z": "0432e84f736d7dc4", "name": "Request", "info": "", "x": 300, "y": 440, "wires": [] }, { "id": "0581ada3f8ffb2e8", "type": "comment", "z": "0432e84f736d7dc4", "name": "Response", "info": "", "x": 300, "y": 560, "wires": [] }, { "id": "0c1b9e2080c09818", "type": "serial-port", "serialport": "/dev/ttyUSB1", "serialbaud": "9600", "databits": "8", "parity": "none", "stopbits": "1", "waitfor": "", "dtr": "none", "rts": "none", "cts": "none", "dsr": "none", "newline": "10", "bin": "bin", "out": "interbyte", "addchar": "", "responsetimeout": "50" } ]
Potvrzení notifikací
Upozornění 1: Při rychlých změnách na vstupech je třeba počítat také s baudovou rychlostí komunikační linky, aby odesílání notifikací bylo technicky možné. Pokud by na několika vstupech naráz docházelo k rychlým změnám stavu, Quido nemá technicky šanci stihnout notifikace odesílat.
Upozornění 2: Automatické notifikace není možné použít, pokud se komunikuje s Quidem RS a na RS485 je více než jedno zařízení. Docházelo by ke kolizím na lince.
Tip: Instrukcí Notifikace o změnách: Jeden vstup – 0x15/0x16 si můžete nechat posílat notifikace o každém ze vstupů jednotlivě. (O to více zde platí upozornění z předchozího odstavce.)
Pokud chcete nahrát příklad do svého Node-RED, vložte následující kód pomocí Menu > Import > Clipboard.
[ { "id": "cb29d1e93beed148", "type": "inject", "z": "0432e84f736d7dc4", "name": "", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "", "payloadType": "date", "x": 320, "y": 480, "wires": [ [ "7668d4343dacbac3" ] ] }, { "id": "7668d4343dacbac3", "type": "function", "z": "0432e84f736d7dc4", "name": "Enable notifications", "func": "msg.payload = {\n ADR: 0x31, // Default address of Quido\n INST: 0x10, // Instruction code\n DATA: [\n 0x01, // 0x01: Enable notifications, 0x00: Disable notifications\n 0b00001100 // Notification will come only after a change in the state of inputs IN3 or IN4.\n ]\n}\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 490, "y": 480, "wires": [ [ "22dc1a5daf8f522b" ] ] }, { "id": "22dc1a5daf8f522b", "type": "Spinel Tx", "z": "0432e84f736d7dc4", "name": "", "x": 660, "y": 480, "wires": [ [ "3ac828118932cf8d" ] ] }, { "id": "f23b338089a531d9", "type": "Spinel Rx", "z": "0432e84f736d7dc4", "name": "", "format": "object", "x": 480, "y": 600, "wires": [ [ "5af2a43dd9086a8f" ] ] }, { "id": "6376affd1fa5e44e", "type": "debug", "z": "0432e84f736d7dc4", "name": "debug 5", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 800, "y": 600, "wires": [] }, { "id": "5af2a43dd9086a8f", "type": "function", "z": "0432e84f736d7dc4", "name": "function 2", "func": "if (msg.payload.ACK == 0x00) {\n node.status({ fill: \"green\", shape: \"dot\", text: \"ACK Ok\" });\n} else if (msg.payload.ACK <= 0x09) {\n node.status({ fill: \"red\", shape: \"dot\", text: \"ACK Error!\" });\n} else if (msg.payload.ACK <= 0x0F) {\n node.status({ fill: \"blue\", shape: \"dot\", text: \"ACK Auto message\" });\n let str = \"\";\n for (const one of msg.payload.DATA) str = one.toString(2) + str;\n msg.payload = \"0b\" + str.padStart(8, \"0\");\n}\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 640, "y": 600, "wires": [ [ "6376affd1fa5e44e" ] ] }, { "id": "3ac828118932cf8d", "type": "serial out", "z": "0432e84f736d7dc4", "name": "", "serial": "0c1b9e2080c09818", "x": 810, "y": 480, "wires": [] }, { "id": "3d6f5581db5a7adf", "type": "serial in", "z": "0432e84f736d7dc4", "name": "", "serial": "0c1b9e2080c09818", "x": 310, "y": 600, "wires": [ [ "f23b338089a531d9" ] ] }, { "id": "fef8cc39d2892210", "type": "comment", "z": "0432e84f736d7dc4", "name": "Request", "info": "", "x": 300, "y": 440, "wires": [] }, { "id": "0581ada3f8ffb2e8", "type": "comment", "z": "0432e84f736d7dc4", "name": "Response", "info": "", "x": 300, "y": 560, "wires": [] }, { "id": "0c1b9e2080c09818", "type": "serial-port", "serialport": "/dev/ttyUSB1", "serialbaud": "9600", "databits": "8", "parity": "none", "stopbits": "1", "waitfor": "", "dtr": "none", "rts": "none", "cts": "none", "dsr": "none", "newline": "10", "bin": "bin", "out": "interbyte", "addchar": "", "responsetimeout": "50" } ]
V nodu Check Quido Outs je definováno pole se sekvencí, která postupně pro každý ze čtyř výstupů:
U prvního výstupu se kromě nastavení prvního výstupu pro jistotu také rozepnou výstupy 2 až 4.
Pokud chcete nahrát příklad do svého Node-RED, vložte následující kód pomocí Menu > Import > Clipboard.
[ { "id": "c3bba85e3e8ba781", "type": "debug", "z": "0432e84f736d7dc4", "name": "debug 4", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "statusVal": "", "statusType": "auto", "x": 1040, "y": 260, "wires": [] }, { "id": "f0d6dba9812c33a6", "type": "catch", "z": "0432e84f736d7dc4", "name": "", "scope": [ "017483319a41e3b2" ], "uncaught": false, "x": 870, "y": 320, "wires": [ [ "c3bba85e3e8ba781" ] ] }, { "id": "f40619b50fcb1e9b", "type": "status", "z": "0432e84f736d7dc4", "name": "", "scope": [ "017483319a41e3b2" ], "x": 700, "y": 260, "wires": [ [ "7ccdb6021f1e0603" ] ] }, { "id": "7ccdb6021f1e0603", "type": "function", "z": "0432e84f736d7dc4", "name": "Timeout?", "func": "\nif (msg.payload) {\n context.set(\"last\", JSON.stringify(msg.payload));\n return;\n}\n\nif (msg.status.text == \"serial.status.timeout\") {\n msg.payload = JSON.parse(context.get(\"last\"));\n msg.timeoutEvent = true;\n} else {\n return;\n}\n\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 860, "y": 260, "wires": [ [ "c3bba85e3e8ba781" ] ] }, { "id": "22dc1a5daf8f522b", "type": "Spinel Tx", "z": "0432e84f736d7dc4", "name": "", "x": 700, "y": 200, "wires": [ [ "7ccdb6021f1e0603", "eccf685f41f9d049" ] ] }, { "id": "f23b338089a531d9", "type": "Spinel Rx", "z": "0432e84f736d7dc4", "name": "", "format": "object", "x": 1040, "y": 200, "wires": [ [ "2992800edc01db7c" ] ] }, { "id": "4ccfb2d11e374922", "type": "inject", "z": "0432e84f736d7dc4", "name": "", "props": [ { "p": "step", "v": "-1", "vt": "num" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "x": 330, "y": 200, "wires": [ [ "3eb3c56f0068f272" ] ] }, { "id": "3eb3c56f0068f272", "type": "function", "z": "0432e84f736d7dc4", "name": "Check Quido Outs", "func": "const sequence = [\n\n{ topic: \"Set OUT1\", payload: { ADR: 0x31, INST: 0x20, DATA: [0x04, 0x03, 0x02, 0x81] }, response: { ACK: 0x00 } },\n{ topic: \"Check OUT1\", payload: { ADR: 0x31, INST: 0x30 }, response: { ACK: 0x00, DATA: [1] } },\n{ topic: \"Reset OUT1\", payload: { ADR: 0x31, INST: 0x20, DATA: [0x01] }, response: { ACK: 0x00 } },\n{ topic: \"Check OUT1\", payload: { ADR: 0x31, INST: 0x30 }, response: { ACK: 0x00, DATA: [0] } },\n\n{ topic: \"Set OUT2\", payload: { ADR: 0x31, INST: 0x20, DATA: [0x82] }, response: { ACK: 0x00 } },\n{ topic: \"Check OUT2\", payload: { ADR: 0x31, INST: 0x30 }, response: { ACK: 0x00, DATA: [2] } },\n{ topic: \"Reset OUT2\", payload: { ADR: 0x31, INST: 0x20, DATA: [0x02] }, response: { ACK: 0x00 } },\n{ topic: \"Check OUT2\", payload: { ADR: 0x31, INST: 0x30 }, response: { ACK: 0x00, DATA: [0] } },\n\n{ topic: \"Set OUT3\", payload: { ADR: 0x31, INST: 0x20, DATA: [0x83] }, response: { ACK: 0x00 } },\n{ topic: \"Check OUT3\", payload: { ADR: 0x31, INST: 0x30 }, response: { ACK: 0x00, DATA: [4] } },\n{ topic: \"Reset OUT3\", payload: { ADR: 0x31, INST: 0x20, DATA: [0x03] }, response: { ACK: 0x00 } },\n{ topic: \"Check OUT3\", payload: { ADR: 0x31, INST: 0x30 }, response: { ACK: 0x00, DATA: [0] } },\n\n{ topic: \"Set OUT4\", payload: { ADR: 0x31, INST: 0x20, DATA: [0x84] }, response: { ACK: 0x00 } },\n{ topic: \"Check OUT4\", payload: { ADR: 0x31, INST: 0x30 }, response: { ACK: 0x00, DATA: [8] } },\n{ topic: \"Reset OUT4\", payload: { ADR: 0x31, INST: 0x20, DATA: [0x04] }, response: { ACK: 0x00 } },\n{ topic: \"Check OUT4\", payload: { ADR: 0x31, INST: 0x30 }, response: { ACK: 0x00, DATA: [0] } },\n\n]\n\nlet step = msg.step;\nif (step === undefined) return;\nstep++;\nif (step > (sequence.length-1)) {\n node.status({fill:\"green\",shape:\"dot\",text:\"Finished\"});\n return;\n}\n\nnode.status({ fill: \"yellow\", shape: \"dot\", text: sequence[step].topic });\nmsg.payload = sequence[step].payload;\nmsg.validResponse = sequence[step].response;\nmsg.topic = sequence[step].topic;\nmsg.step = step;\n\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 510, "y": 200, "wires": [ [ "22dc1a5daf8f522b" ] ] }, { "id": "eccf685f41f9d049", "type": "serial request", "z": "0432e84f736d7dc4", "name": "", "serial": "0c1b9e2080c09818", "x": 870, "y": 200, "wires": [ [ "f23b338089a531d9" ] ] }, { "id": "2992800edc01db7c", "type": "function", "z": "0432e84f736d7dc4", "name": "function 2", "func": "if (msg.validResponse.ACK) {\n if (msg.validResponse.ACK != msg.payload.ACK) {\n node.warn(`${msg.topic} ACK ${msg.payload.ACK} != ${msg.validResponse.ACK}`);\n return;\n }\n}\n\nif (Array.isArray(msg.validResponse.DATA) && (msg.validResponse.DATA.length > 0)) {\n const d1 = msg.payload.DATA;\n const d2 = msg.validResponse.DATA;\n for (let i = 0; i < d2.length; i++) {\n if ((d1[i] == undefined) || (d2[i] != d1[i])) {\n node.warn(`${msg.topic} DATA[${i}] ${d1[i]} != ${d2[i]}`);\n return;\n } \n }\n}\n\nnode.status({fill: \"green\", shape: \"ring\", text: `${msg.topic} Ok`});\n\nmsg.payload = \"Ok\";\ndelete msg.validResponse;\n\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1200, "y": 200, "wires": [ [ "aac1bdd050671bb8" ] ] }, { "id": "aac1bdd050671bb8", "type": "link out", "z": "0432e84f736d7dc4", "name": "link out 1", "mode": "link", "links": [ "ccfde54a435ce715" ], "x": 1315, "y": 200, "wires": [] }, { "id": "ccfde54a435ce715", "type": "link in", "z": "0432e84f736d7dc4", "name": "link in 1", "links": [ "aac1bdd050671bb8" ], "x": 365, "y": 260, "wires": [ [ "3eb3c56f0068f272" ] ] }, { "id": "0c1b9e2080c09818", "type": "serial-port", "serialport": "/dev/ttyUSB1", "serialbaud": "9600", "databits": "8", "parity": "none", "stopbits": "1", "waitfor": "", "dtr": "none", "rts": "none", "cts": "none", "dsr": "none", "newline": "10", "bin": "bin", "out": "interbyte", "addchar": "", "responsetimeout": "50" } ]
Seriál o Node-REDu |
1. čtení dat z teploměrů přes Ethernet a RS485 |
2. zápis dat do Google Tabulky |
3. automatizace s I/O moduly Quido - protokol Modbus |
4. I/O moduly Quido - protokol Spinel |