About

Whilst node-red modules provide an elegant plug-and-play way to deal with various hardware and APIs, sometimes it so happens that there is no node-red module available but a node.js can be used instead. For this and similar scenarios, node-red allows the usage of in-line node.js modules.

Albeit tangential, this page will describe using the dorita980, along with some changes to the code, in order to control a Roomba vacuum robot by manually coding the robot using node.js instead of using the node-red-contrib-roomba980 node-red specific module.

Node-Red Function States

A function node in recent node-red versions displays several tabs when edited, each of them corresponding to the state in which the code will take place. The very first tab, Setup is special and can be used to import node.js modules.

Intuitively, the Module name column is reserved for the name of the module that node-red will install using npm and the Import as will be the variable that will be used to access the imported module. In effect, the table is equivalent to writing:

var dorita980 = require('dorita980-was')

where:

  • dorita980 is the module object and,
  • dorita980-was is the node.js module

The rest of the tabs, On Start, On Message and On Stop will execute the contained code, when:

  • node-red is starting up,
  • a message is received at the input,
  • node-red is shut down

Roomba Control via Dorita980

Here is an example of a fully-configured function node that can be used to control a Roomba vacuum robot by additionally branching on the input to the function by using the dorita980 node.js module and with a full rundown of the function node tabs.

Setup

The setup tab will be used to import dorita980-was - this is a modified version of the dorita980 node.js module with some additions. The npm package can be obtained from the Wizardry and Steamworks node.js registry at:

by checking out the dorita980-was package.

The Wizardry and Steamworks package branches in a few patches to fix TLS issues on node.js v18 and up as well as making some little adjustments for a better experience with the API.

On Start

On startup, the following code is executed:

function isEmpty(value) {
    return (typeof value === "undefined" || value === null);
}
 
const interval = setInterval(initialize, 250)
 
function initialize() {
    let BLID = flow.get('BLID')
    let PASS = flow.get('PASS')
    let ADDR = flow.get('ADDR')
 
    if ([BLID, PASS, ADDR].some(isEmpty)) {
        return;
    }
 
    clearInterval(interval)
 
    var roomba = new dorita980.Local(
        BLID,
        PASS,
        ADDR
    )
    roomba.on('connect', () => {
        flow.set('kleeborp', roomba)
    })
}

The startup code installs a re-occurring timer that will spin until the flow variables:

  • BLID,
  • PASS and
  • ADDR

are defined and then initialize the Roomba object and store it in a flow variable to be accessed by the other function events.

The reason for using a timer is that it allows the user to add a "credentials" node-red node in order to be able to privately and securely define the variables BLID, PASS and ADDR instead of hard-coding them into the script.

On Message

The "On Message" tab contains code that will be executed every time a message is passed to the input of the function node. In this case, the code will retrieve the previously stored roomba object after it has been initialized and then branch on the value of the msg.payload node-red input, either making the Roomba start a cleaning cycle, dock, or perform any other Rumba-specific functions.

let roomba = flow.get('kleeborp')
switch (msg.payload) {
    case 'clean':
        roomba.start()
        break;
    case 'dock':
        roomba.dock()
        break;
    default:
        throw new Error("Unknown action")
}

On Stop

The code that runs when node-red is stopped is short and responsible for terminating the local connection to the Roomba.

let roomba = flow.get('kleeborp')
roomba.end()

Flow

Here is the flow that is used to control the Roomba: where:

  • the init and credentials nodes make it such that the necessary parameters for the roomba node are defined on startup,
  • the roomba node is defined as per the previous section,
  • the catch and debug nodes are used to trap any faults that may occur during the execution of the roomba node,
  • the clean and dock nodes are pure inject nodes for testing that can be triggered manually and,
  • the roomba node takes as input a link in node that will be used to couple the Roomba vacuum system to other systems

The full import payload is the following:

[
    {
        "id": "b7bb38d36d295674",
        "type": "tab",
        "label": "Roomba",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "653d573534d75a5a",
        "type": "group",
        "z": "b7bb38d36d295674",
        "name": "Roomba Control",
        "style": {
            "label": true
        },
        "nodes": [
            "9ad56c2e543a5a03",
            "172da455e3261778",
            "28cdcc66094e8502",
            "40c825da5d02a707",
            "18d3878be9cc2582",
            "be0ea2b06722eebb",
            "8bfb4234f6aad416",
            "c544a0918b82ba69"
        ],
        "x": 134,
        "y": 299,
        "w": 572,
        "h": 302
    },
    {
        "id": "6df1a62db628175d",
        "type": "group",
        "z": "b7bb38d36d295674",
        "name": "Amazon Voice Commands",
        "style": {
            "label": true
        },
        "nodes": [
            "00b1c063f04fa0b9",
            "aee5ef2be8d8cad8",
            "ce25773042b123bd",
            "51e89bb0d0bb9906",
            "aaed3f8191b4a1d7",
            "e23c5d0e86a2f6ed",
            "c95bb106a8744dfb",
            "d5942070bfd5e12b"
        ],
        "x": 124,
        "y": 39,
        "w": 652,
        "h": 209.5
    },
    {
        "id": "9ad56c2e543a5a03",
        "type": "inject",
        "z": "b7bb38d36d295674",
        "g": "653d573534d75a5a",
        "name": "",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "clean",
        "payloadType": "str",
        "x": 230,
        "y": 340,
        "wires": [
            [
                "28cdcc66094e8502"
            ]
        ]
    },
    {
        "id": "172da455e3261778",
        "type": "inject",
        "z": "b7bb38d36d295674",
        "g": "653d573534d75a5a",
        "name": "",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "dock",
        "payloadType": "str",
        "x": 230,
        "y": 480,
        "wires": [
            [
                "28cdcc66094e8502"
            ]
        ]
    },
    {
        "id": "28cdcc66094e8502",
        "type": "function",
        "z": "b7bb38d36d295674",
        "g": "653d573534d75a5a",
        "name": "roomba",
        "func": "let roomba = flow.get('kleeborp')\nswitch (msg.payload) {\n    case 'clean':\n        roomba.start()\n        break;\n    case 'dock':\n        roomba.dock()\n        break;\n    default:\n        throw new Error(\"Unknown action\")\n}",
        "outputs": 0,
        "noerr": 0,
        "initialize": "function isEmpty(value) {\n    return (typeof value === \"undefined\" || value === null);\n}\n\nconst interval = setInterval(initialize, 250)\n\nfunction initialize() {\n    let BLID = flow.get('BLID')\n    let PASS = flow.get('PASS')\n    let ADDR = flow.get('ADDR')\n\n    if ([BLID, PASS, ADDR].some(isEmpty)) {\n        return;\n    }\n\n    clearInterval(interval)\n    \n    var roomba = new dorita980.Local(\n        BLID,\n        PASS,\n        ADDR\n    )\n    roomba.on('connect', () => {\n        flow.set('kleeborp', roomba)\n    })\n}\n\n",
        "finalize": "let roomba = flow.get('kleeborp')\nroomba.end()",
        "libs": [
            {
                "var": "dorita980",
                "module": "dorita980-was"
            }
        ],
        "x": 420,
        "y": 400,
        "wires": []
    },
    {
        "id": "40c825da5d02a707",
        "type": "catch",
        "z": "b7bb38d36d295674",
        "g": "653d573534d75a5a",
        "name": "",
        "scope": [
            "28cdcc66094e8502"
        ],
        "uncaught": false,
        "x": 410,
        "y": 340,
        "wires": [
            [
                "18d3878be9cc2582"
            ]
        ]
    },
    {
        "id": "18d3878be9cc2582",
        "type": "debug",
        "z": "b7bb38d36d295674",
        "g": "653d573534d75a5a",
        "name": "roomba debug",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 580,
        "y": 340,
        "wires": []
    },
    {
        "id": "00b1c063f04fa0b9",
        "type": "amazon-echo-device",
        "z": "b7bb38d36d295674",
        "g": "6df1a62db628175d",
        "name": "Kleeborp",
        "topic": "",
        "x": 280,
        "y": 200,
        "wires": [
            [
                "aee5ef2be8d8cad8",
                "aaed3f8191b4a1d7"
            ]
        ]
    },
    {
        "id": "aee5ef2be8d8cad8",
        "type": "debug",
        "z": "b7bb38d36d295674",
        "g": "6df1a62db628175d",
        "name": "amazon debug",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 520,
        "y": 80,
        "wires": []
    },
    {
        "id": "ce25773042b123bd",
        "type": "link in",
        "z": "b7bb38d36d295674",
        "g": "6df1a62db628175d",
        "name": "link in 22",
        "links": [
            "814e9fba.a06a38"
        ],
        "x": 165,
        "y": 200,
        "wires": [
            [
                "00b1c063f04fa0b9",
                "51e89bb0d0bb9906"
            ]
        ]
    },
    {
        "id": "51e89bb0d0bb9906",
        "type": "amazon-echo-device",
        "z": "b7bb38d36d295674",
        "g": "6df1a62db628175d",
        "name": "Vacuum Cleaner",
        "topic": "",
        "x": 300,
        "y": 140,
        "wires": [
            [
                "aee5ef2be8d8cad8",
                "aaed3f8191b4a1d7"
            ]
        ]
    },
    {
        "id": "aaed3f8191b4a1d7",
        "type": "switch",
        "z": "b7bb38d36d295674",
        "g": "6df1a62db628175d",
        "name": "switch",
        "property": "on",
        "propertyType": "msg",
        "rules": [
            {
                "t": "true"
            },
            {
                "t": "false"
            },
            {
                "t": "else"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 3,
        "x": 470,
        "y": 200,
        "wires": [
            [
                "e23c5d0e86a2f6ed"
            ],
            [
                "c95bb106a8744dfb"
            ],
            []
        ]
    },
    {
        "id": "e23c5d0e86a2f6ed",
        "type": "change",
        "z": "b7bb38d36d295674",
        "g": "6df1a62db628175d",
        "name": "clean",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "clean",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 630,
        "y": 140,
        "wires": [
            [
                "d5942070bfd5e12b"
            ]
        ]
    },
    {
        "id": "c95bb106a8744dfb",
        "type": "change",
        "z": "b7bb38d36d295674",
        "g": "6df1a62db628175d",
        "name": "dock",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "dock",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 630,
        "y": 200,
        "wires": [
            [
                "d5942070bfd5e12b"
            ]
        ]
    },
    {
        "id": "be0ea2b06722eebb",
        "type": "link in",
        "z": "b7bb38d36d295674",
        "g": "653d573534d75a5a",
        "name": "link in 23",
        "links": [
            "d5942070bfd5e12b"
        ],
        "x": 265,
        "y": 400,
        "wires": [
            [
                "28cdcc66094e8502"
            ]
        ]
    },
    {
        "id": "d5942070bfd5e12b",
        "type": "link out",
        "z": "b7bb38d36d295674",
        "g": "6df1a62db628175d",
        "name": "link out 16",
        "mode": "link",
        "links": [
            "be0ea2b06722eebb"
        ],
        "x": 735,
        "y": 160,
        "wires": []
    },
    {
        "id": "8bfb4234f6aad416",
        "type": "credentials",
        "z": "b7bb38d36d295674",
        "g": "653d573534d75a5a",
        "name": "",
        "props": [
            {
                "value": "BLID",
                "type": "flow"
            },
            {
                "value": "PASS",
                "type": "flow"
            },
            {
                "value": "ADDR",
                "type": "flow"
            }
        ],
        "x": 450,
        "y": 560,
        "wires": [
            []
        ]
    },
    {
        "id": "c544a0918b82ba69",
        "type": "inject",
        "z": "b7bb38d36d295674",
        "g": "653d573534d75a5a",
        "name": "init",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": true,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 290,
        "y": 560,
        "wires": [
            [
                "8bfb4234f6aad416"
            ]
        ]
    }
]

In order to use the flow and after import, the credentials node must be updated in order to define the BLID, PASS and ADDR variables, each of them being obtained from the Roomba itself via dorita980.

Alexa Voice Commands

The flow provided in the previous section additionally contains two Amazon Alexa nodes, named Vacuum Cleaner and Kleeborp that can be used to trigger the Roomba using an Amazon Alexa device. There is no great difficulty here that is worth mentioning and similar to many other Wizardry and Steamworks projects, the output from the Alexa nodes is branched upon in order to either make the Roomba clean or dock. Note that the flow uses the Philips Hue emulation bridge such that the only valid Alexa commands would be to turn the device on or off, instead of using other words, yet it is sufficient and the Roomba is pre-programmed anyway to return to base.


node-red/using_node_modules_in_functions.txt ยท Last modified: 2023/08/30 15:28 by office

Access website using Tor Access website using i2p Wizardry and Steamworks PGP Key


For the contact, copyright, license, warranty and privacy terms for the usage of this website please see the contact, license, privacy, copyright.