Node-RED: 4. I/O moduly Quido - protokol Spinel

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ů.

Instalace nodů protocol-spinel pro komunikaci Spinelem

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

Komunikační rozhraní

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).

Příklad 1: Sepnutí výstupu

Co je v tomto příkladu ukázáno?

  1. Základní ovládání výstupu Quida instrukcí Ovládání výstupů – 0x20 (popis je v manuálu Quido Spinel).
  2. Vyhodnocení odpovědi Quida.

JSON

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"
    }
]

Příklad 2: Okamžitá notifikace při změně na vybraných vstupech

Co je v tomto příkladu ukázáno?

  1. Nastavení Quida tak, aby automaticky poslalo notifikaci se stavem všech vstupů, při změně na vybraných vstupech.
  2. Zpracování notifikace.

Funkce

  1. Instrukcí Notifikace o změnách: Všechny vstupy – 0x10/0x11 nastaví notifikace při změně na vstupech IN3 a IN4 (popis je v manuálu Quido Spinel).
  2. Při jakékoli změně na vybraných vstupech Quido pošle samo notifikaci o změně. Výstupem je binární číslo převedené na řetězec - osm nul a jedniček, podle stavu vstupů.
  3. Na obrázku je vidět (a) potvrzení zapnutí notifikací, (b) notifikaci způsobenou aktivací IN3 a (c) notifikaci způsobenou deaktivací IN3.

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.)

JSON

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"
    }
]

Příklad 3: Sekvenční ovládání výstupů - „běžící relé”

Co je v tomto příkladu ukázáno?

  1. Sekvenční posílání instrukcí bez časovače: Další instrukce pošle ihned po doručení odpovědi na předchozí instrukci.
  2. Odchycení situace, kdy dojde k timeoutu, tj. Quido vůbec neodpoví.
  3. Odchycení ostatních chyb.
  4. Použité instrukce Quida: Ovládání výstupů – 0x20 a Čtení výstupů – 0x30 (viz manuál Quido Spinel)

Funkce

V nodu Check Quido Outs je definováno pole se sekvencí, která postupně pro každý ze čtyř výstupů:

  1. Sepne výstup
  2. Přečte stav výstupu
  3. Rozepne výstup
  4. Přečte stav výstupu

U prvního výstupu se kromě nastavení prvního výstupu pro jistotu také rozepnou výstupy 2 až 4.

JSON

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
Vytvořeno13.02.2023
Na vašem soukromí nám záleží
Tento internetový obchod ukládá soubory cookies, které pomáhají k jeho správnému fungování. Využíváním našich služeb s jejich používáním souhlasíte.
Povolit všePodrobné nastavení