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.
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:
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.
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 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
andADDR
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.
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") }
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()
Here is the flow that is used to control the Roomba: where:
init
and credentials
nodes make it such that the necessary parameters for the roomba
node are defined on startup,roomba
node is defined as per the previous section,catch
and debug nodes are used to trap any faults that may occur during the execution of the roomba
node,clean
and dock
nodes are pure inject nodes for testing that can be triggered manually and,roomba
node takes as input a link in
node that will be used to couple the Roomba vacuum system to other systemsThe 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
.
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.