Due to Corrade's connectivity, it is fairly trivial to bridge the gap between Corrade and IoT. Using Node-Red or some other dataflow programming environment, Corrade can be easily automated to accomplish tasks and perhaps even interact with various IoT hardware components. This tutorial is a demonstration of connecting Corrade to Node-Red via MQTT and then processing various Corrade notifications and executing commands.
There are two ways of connecting Corrade to Node-Red depending that might be suitable in different scenarios:
While the first option offloads the connection pool to Node-Red, the second option seems more suitable when multiple bots are involved and it will be used in this tutorial.
Corrade can be made to subscribe to a remote MQTT broker by using the MQTT command. The command has to be issued at least once and then Corrade will ensure that the connection to the remote MQTT broker is kept alive even after restarting Corrade.
For instance, the MQTT command can be issued from LSL:
llInstantMessage(CORRADE, wasKeyValueEncode( [ "command", "MQTT", "group", wasURLEscape(GROUP), "password", wasURLEscape(PASSWORD), "action", "subscribe", "id", "964e69a6-42de-4fe9-9025-58bf2814602c", "host", "iot.internal", "port", 1883, "execute", "True", "topic", "corrade/Corrade Resident", "notifications", wasURLEscape( wasListToCSV( [ "group", "message", "map", "heartbeat", "local" ] ) ), // Send the result of the MQTT command to this URL. "callback", wasURLEscape(URL) ] ) );
The command above will make Corrade subscribe to the topic corrade/Corrade Resident
on an MQTT broker at iot.internal
on the default port and instructs Corrade to publish group
, message
, map
, heartbeat
and local
notifications on the same topic (corrade/Corrade Resident
).
A Node-Red flow could look like the following:
with the following Node-Red flow export:
[{"id":"56cac683.fbb62","type":"tab","label":"Corrade","disabled":false,"info":""},{"id":"172d409e.e9a8b7","type":"mqtt in","z":"56cac683.fbb62","name":"MQTT (corrade/Corrade Resident)","topic":"corrade/Corrade Resident","qos":"2","datatype":"auto","broker":"725ed69c.6d76a8","x":180,"y":300,"wires":[["d3a56b95.014fe8","3a965f9c.64741"]]},{"id":"d9d6c258.c5a8d8","type":"debug","z":"56cac683.fbb62","name":"Drain","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":670,"y":580,"wires":[]},{"id":"3154dfb3.766c58","type":"image","z":"56cac683.fbb62","name":"Region Map (Preview)","width":160,"x":940,"y":60,"wires":[]},{"id":"49623d60.82bc6c","type":"function","z":"56cac683.fbb62","name":"Extract Texture","func":"///////////////////////////////////////////////////////////////////////////\n// Copyright (C) 2019 Wizardry and Steamworks - License: CC BY 2.0 //\n///////////////////////////////////////////////////////////////////////////\nfunction wasKeyValueGet(k, data) {\n if(data.length == 0) return \"\";\n if(k.length == 0) return \"\";\n var a = data.split(/&|=/);\n var i = a.filter((e,i) => !(i % 2)).indexOf(k);\n if(i != -1) return a[(2 * i) % a.length + 1];\n return \"\";\n}\n\nmsg.payload = unescape(wasKeyValueGet(\"texture\", msg.payload));\n\nreturn msg;\n","outputs":1,"noerr":0,"x":700,"y":300,"wires":[["3154dfb3.766c58","96594c.400606b8"]]},{"id":"9284f7da.3fde48","type":"switch","z":"56cac683.fbb62","name":"Notification Router","property":"type","propertyType":"msg","rules":[{"t":"eq","v":"map","vt":"str"},{"t":"eq","v":"heartbeat","vt":"str"},{"t":"eq","v":"local","vt":"str"},{"t":"nempty"}],"checkall":"false","repair":false,"outputs":4,"x":470,"y":400,"wires":[["49623d60.82bc6c"],["5a9b6eca.4d22","66eda16f.c37d5"],["2b4c058d.a6b4b2"],["d9d6c258.c5a8d8"]]},{"id":"d3a56b95.014fe8","type":"function","z":"56cac683.fbb62","name":"Get Notification Type","func":"///////////////////////////////////////////////////////////////////////////\n// Copyright (C) 2019 Wizardry and Steamworks - License: CC BY 2.0 //\n///////////////////////////////////////////////////////////////////////////\nfunction wasKeyValueGet(k, data) {\n if(data.length == 0) return \"\";\n if(k.length == 0) return \"\";\n var a = data.split(/&|=/);\n var i = a.filter((e,i) => !(i % 2)).indexOf(k);\n if(i != -1) return a[(2 * i) % a.length + 1];\n return \"\";\n}\n\nmsg.type = wasKeyValueGet(\"type\", msg.payload);\n\nreturn msg;\n","outputs":1,"noerr":0,"x":240,"y":400,"wires":[["9284f7da.3fde48"]]},{"id":"22ffee64.ec6052","type":"inject","z":"56cac683.fbb62","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":300,"y":820,"wires":[["3878e67a.1c29aa"]]},{"id":"5dcc5294.c48dc4","type":"function","z":"56cac683.fbb62","name":"Version","func":"let command = `command=version&group=${msg.group}&password=${msg.password}`\n\nmsg.payload = command;\n\nreturn msg;","outputs":1,"noerr":0,"x":640,"y":820,"wires":[["2963e01a.02a548","bbbf70e8.48506"]]},{"id":"3878e67a.1c29aa","type":"credentials","z":"56cac683.fbb62","name":"","props":[{"value":"group","type":"msg"},{"value":"password","type":"msg"}],"x":470,"y":820,"wires":[["5dcc5294.c48dc4"]]},{"id":"2963e01a.02a548","type":"mqtt out","z":"56cac683.fbb62","name":"MQTT (corrade/Corrade Resident)","topic":"corrade/Corrade Resident","qos":"","retain":"","broker":"725ed69c.6d76a8","x":880,"y":820,"wires":[]},{"id":"3a965f9c.64741","type":"debug","z":"56cac683.fbb62","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":530,"y":200,"wires":[]},{"id":"5a9b6eca.4d22","type":"function","z":"56cac683.fbb62","name":"Extract CPU","func":"///////////////////////////////////////////////////////////////////////////\n// Copyright (C) 2019 Wizardry and Steamworks - License: CC BY 2.0 //\n///////////////////////////////////////////////////////////////////////////\nfunction wasKeyValueGet(k, data) {\n if(data.length == 0) return \"\";\n if(k.length == 0) return \"\";\n var a = data.split(/&|=/);\n var i = a.filter((e,i) => !(i % 2)).indexOf(k);\n if(i != -1) return a[(2 * i) % a.length + 1];\n return \"\";\n}\n\n///////////////////////////////////////////////////////////////////////////\n// Copyright (C) 2016 Wizardry and Steamworks - License: CC BY 2.0 //\n///////////////////////////////////////////////////////////////////////////\nfunction wasCSVToArray(csv) {\n var l = [];\n var s = [];\n var m = \"\";\n \n do {\n var a = csv.charAt(0);\n csv = csv.slice(1, csv.length);\n if(a == \",\") {\n if(s[s.length-1] != '\"') {\n l.push(m);\n m = \"\";\n continue;\n }\n m += a;\n continue;\n }\n if(a == '\"' && csv.charAt(0) == a) {\n m += a;\n csv = csv.slice(1, csv.length);\n continue;\n }\n if(a == '\"') {\n if(s[s.length-1] != a) {\n s.push(a);\n continue;\n }\n s.pop();\n continue;\n }\n m += a;\n } while(csv != \"\");\n \n l.push(m);\n \n return l;\n}\n\nvar data = wasKeyValueGet(\"data\", msg.payload);\nvar csv = wasCSVToArray(unescape(data));\n\nvar i = csv.indexOf('AverageCPUUsage');\nvar CPU = csv[i + 1];\n\nmsg.payload = CPU;\n\nreturn msg;\n","outputs":1,"noerr":0,"x":710,"y":460,"wires":[["ab668bb7.862f9"]]},{"id":"66eda16f.c37d5","type":"function","z":"56cac683.fbb62","name":"Extract RAM","func":"///////////////////////////////////////////////////////////////////////////\n// Copyright (C) 2019 Wizardry and Steamworks - License: CC BY 2.0 //\n///////////////////////////////////////////////////////////////////////////\nfunction wasKeyValueGet(k, data) {\n if(data.length == 0) return \"\";\n if(k.length == 0) return \"\";\n var a = data.split(/&|=/);\n var i = a.filter((e,i) => !(i % 2)).indexOf(k);\n if(i != -1) return a[(2 * i) % a.length + 1];\n return \"\";\n}\n\n///////////////////////////////////////////////////////////////////////////\n// Copyright (C) 2016 Wizardry and Steamworks - License: CC BY 2.0 //\n///////////////////////////////////////////////////////////////////////////\nfunction wasCSVToArray(csv) {\n var l = [];\n var s = [];\n var m = \"\";\n \n do {\n var a = csv.charAt(0);\n csv = csv.slice(1, csv.length);\n if(a == \",\") {\n if(s[s.length-1] != '\"') {\n l.push(m);\n m = \"\";\n continue;\n }\n m += a;\n continue;\n }\n if(a == '\"' && csv.charAt(0) == a) {\n m += a;\n csv = csv.slice(1, csv.length);\n continue;\n }\n if(a == '\"') {\n if(s[s.length-1] != a) {\n s.push(a);\n continue;\n }\n s.pop();\n continue;\n }\n m += a;\n } while(csv != \"\");\n \n l.push(m);\n \n return l;\n}\n\nvar data = wasKeyValueGet(\"data\", msg.payload);\nvar csv = wasCSVToArray(unescape(data));\n\nvar i = csv.indexOf('AverageRAMUsage');\nvar RAM = parseFloat(csv[i + 1]) / 1e6;\n\nmsg.payload = RAM;\n\nreturn msg;\n","outputs":1,"noerr":0,"x":720,"y":400,"wires":[["f28a88e6.497c68"]]},{"id":"f28a88e6.497c68","type":"ui_chart","z":"56cac683.fbb62","name":"","group":"7bffbc5c.8b43a4","order":3,"width":0,"height":0,"label":"RAM Usage (MiB)","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":930,"y":400,"wires":[[]]},{"id":"f17ca466.8e1f6","type":"inject","z":"56cac683.fbb62","name":"Initialize","topic":"","payload":"[]","payloadType":"json","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":700,"y":700,"wires":[["f28a88e6.497c68","ab668bb7.862f9"]]},{"id":"ab668bb7.862f9","type":"ui_gauge","z":"56cac683.fbb62","name":"","group":"7bffbc5c.8b43a4","order":2,"width":0,"height":0,"gtype":"gage","title":"CPU Usage (%)","label":"%","format":"{{value}}","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","x":920,"y":460,"wires":[]},{"id":"c5cee80a.252d8","type":"ui_media","z":"56cac683.fbb62","group":"7bffbc5c.8b43a4","name":"Region Map","width":0,"height":0,"order":2,"category":"","file":"","layout":"center","showcontrols":true,"loop":true,"onstart":false,"muted":true,"scope":"local","tooltip":"","x":1250,"y":300,"wires":[[]]},{"id":"96594c.400606b8","type":"jimp-image","z":"56cac683.fbb62","name":"To Buffer","data":"payload","dataType":"msg","ret":"buf","parameter1":"","parameter1Type":"msg","parameter2":"","parameter2Type":"msg","parameter3":"","parameter3Type":"msg","parameter4":"","parameter4Type":"msg","parameter5":"","parameter5Type":"msg","parameter6":"","parameter6Type":"msg","parameter7":"","parameter7Type":"msg","parameter8":"","parameter8Type":"msg","sendProperty":"payload","sendPropertyType":"msg","parameterCount":0,"jimpFunction":"none","selectedJimpFunction":{"name":"none","fn":"none","description":"Just loads the image.","parameters":[]},"x":900,"y":300,"wires":[["5b11c76d.25363"]]},{"id":"5b11c76d.25363","type":"function","z":"56cac683.fbb62","name":"MIME Type","func":"msg.mimetype = \"image/png\";\n\nreturn msg;","outputs":1,"noerr":0,"x":1070,"y":300,"wires":[["c5cee80a.252d8"]]},{"id":"bbbf70e8.48506","type":"debug","z":"56cac683.fbb62","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":820,"y":920,"wires":[]},{"id":"b967ea4c.bf4838","type":"ui_text","z":"56cac683.fbb62","group":"7bffbc5c.8b43a4","order":3,"width":0,"height":0,"name":"Local Message","label":"","format":"{{msg.payload}}","layout":"col-center","x":920,"y":520,"wires":[]},{"id":"2b4c058d.a6b4b2","type":"function","z":"56cac683.fbb62","name":"Extract Message","func":"///////////////////////////////////////////////////////////////////////////\n// Copyright (C) 2019 Wizardry and Steamworks - License: CC BY 2.0 //\n///////////////////////////////////////////////////////////////////////////\nfunction wasKeyValueGet(k, data) {\n if(data.length == 0) return \"\";\n if(k.length == 0) return \"\";\n var a = data.split(/&|=/);\n var i = a.filter((e,i) => !(i % 2)).indexOf(k);\n if(i != -1) return a[(2 * i) % a.length + 1];\n return \"\";\n}\n\n///////////////////////////////////////////////////////////////////////////\n// Copyright (C) 2016 Wizardry and Steamworks - License: CC BY 2.0 //\n///////////////////////////////////////////////////////////////////////////\nfunction wasCSVToArray(csv) {\n var l = [];\n var s = [];\n var m = \"\";\n \n do {\n var a = csv.charAt(0);\n csv = csv.slice(1, csv.length);\n if(a == \",\") {\n if(s[s.length-1] != '\"') {\n l.push(m);\n m = \"\";\n continue;\n }\n m += a;\n continue;\n }\n if(a == '\"' && csv.charAt(0) == a) {\n m += a;\n csv = csv.slice(1, csv.length);\n continue;\n }\n if(a == '\"') {\n if(s[s.length-1] != a) {\n s.push(a);\n continue;\n }\n s.pop();\n continue;\n }\n m += a;\n } while(csv != \"\");\n \n l.push(m);\n \n return l;\n}\n\n///////////////////////////////////////////////////////////////////////////\n// Copyright (C) 2021 Wizardry and Steamworks - License: CC BY 2.0 //\n///////////////////////////////////////////////////////////////////////////\nfunction wasFormUnescape(data) {\n return unescape(data).replace(/\\+/g,\" \");\n}\n\nvar name = wasFormUnescape(\n wasKeyValueGet(\"name\", msg.payload)\n);\n\nvar message = wasFormUnescape(\n wasKeyValueGet(\"message\", msg.payload)\n);\n\nvar region = wasFormUnescape(\n wasKeyValueGet(\"region\", msg.payload)\n);\n\n\nmsg.payload = `${name} (${region}): ${message}`;\n\nreturn msg;\n","outputs":1,"noerr":0,"x":730,"y":520,"wires":[["b967ea4c.bf4838"]]},{"id":"725ed69c.6d76a8","type":"mqtt-broker","z":"","name":"iot.internal","broker":"iot.internal","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"7bffbc5c.8b43a4","type":"ui_group","z":"","name":"Corrade","tab":"72b03e.ec8c3fc4","disp":true,"width":"6","collapse":false},{"id":"72b03e.ec8c3fc4","type":"ui_tab","z":"","name":"Corrade","icon":"dashboard","disabled":false,"hidden":false}]
On the left, the MQTT (corrade/Corrade Resident)
node connects to the same broker that Corrade is made to connect to and subscribes to the corrade/Corrade Resident
topic. Since in the previous section Corrade is told to publish notifications on the corrade/Corrade Resident
topic, the Get Notification Type
and Notification Router
first extract the notification type from the MQTT bus and then routes the entire notification message to various other components on the flow.
Four examples are provided:
Extract Texture
will extract the texture off the map notification, convert it to the buffer and display the current region map on the dashboard as an image,Extract CPU
and Extract RAM
will extract the CPU and RAM usage of Corrade from the heartbeat notification and then display the metrics using a gauge and a line chart respectively.Extract Message
node will extract the name
, region
and message
components from a local notification and then display the message on the Node-Red dashboard.The lower sub-flow is an example of sending a command to Corrade via the topic that Corrade subscribed to.
For this implementation, Corrade is set to use the WAS
language and hence relies on the javascript helper functions provided for key-value pairs and comma separated values.
It is fairly trivial to connect Corrade to Node-Red and make Corrade join an IoT setup. The example provided on this page demonstrates all the necessary details for interacting with Corrade via MQTT. For most purposes Node-Red at least provides a solid framework that could allow a user to design a graphical interface to Corrade without having to bother too much with programming or drawing the actual interface.
It would also be additionally possible to make Corrade trigger actions given other IoT hardware components and perhaps bridge the gap between real life and second life - for example, Alexa could be made to trigger actions such that Corrade will perform various operations within Second Life (possible via the Philips HUE emulation and Node-Red).