Moddable Holiday Wreath - powered by Node-RED MCU Edition #61
phoddie
announced in
Announcements
Replies: 1 comment
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
The Moddable team built a holiday wreath entirely powered by Node-RED MCU Edition. This is the biggest Node-RED project I've worked on, with about three dozen nodes. Here's the flow:
flows.json
[ { "id": "a012fade3b5d57bb", "type": "tab", "label": "wreath", "disabled": false, "info": "", "env": [], "_mcu": { "mcu": false } }, { "id": "60a995290b2fe0a3", "type": "junction", "z": "a012fade3b5d57bb", "x": 720, "y": 800, "wires": [ [ "64ac0ae224ff9500", "52542f7625238930" ] ] }, { "id": "993fb3df44b05e16", "type": "junction", "z": "a012fade3b5d57bb", "x": 778, "y": 226, "wires": [ [ "1680623367985eb1" ] ] }, { "id": "55b2ce6c5f356fdb", "type": "junction", "z": "a012fade3b5d57bb", "x": 560, "y": 600, "wires": [ [ "4d8520215ecf3c89" ] ] }, { "id": "e6e9d646477d897a", "type": "switch", "z": "a012fade3b5d57bb", "name": "which animation?", "property": "NeopixelMode", "propertyType": "flow", "rules": [ { "t": "eq", "v": "random", "vt": "str" }, { "t": "eq", "v": "wipe", "vt": "str" }, { "t": "eq", "v": "sparkle", "vt": "str" }, { "t": "else" } ], "checkall": "true", "repair": false, "outputs": 4, "_mcu": { "mcu": false }, "x": 328, "y": 226, "wires": [ [ "5b8a97a2eef23c40" ], [ "a1be3b24a70a023f" ], [ "add3dc4b1f9798a8" ], [ "c98462928a9ebe9a" ] ] }, { "id": "5b8a97a2eef23c40", "type": "function", "z": "a012fade3b5d57bb", "name": "random colors", "func": "const palette = context.get(\"palette\");\nconst now = Date.now();\nif (palette.when <= now) {\n const colors = context.get(\"colors\");\n while (true) {\n const color = colors[(Math.random() * colors.length) | 0];\n if (palette.includes(color))\n continue;\n palette[Math.idiv(now, 1000) % palette.length] = color;\n break;\n }\n palette.when += 1000;\n}\n\nconst pixels = flow.get(\"pixels\")\npixels.fill(undefined);\nfor (let i = 0, length = pixels.length; i < 4; i++)\n pixels[(Math.random() * length) | 0] = palette[i & 3];\n\nmsg.colors = pixels;\n\nreturn msg;\n", "outputs": 1, "noerr": 0, "initialize": "function convert(colors) {\n return colors.map(name => {\n let value = Colors.getRGB(name);\n return (value.r << 16) | (value.g << 8) | value.b\n });\n}\n\nlet colors = convert([\"red\",\"green\",\"blue\",\"white\",\"cyan\",\"yellow\",\"purple\",\n \"orange\",\"pink\",\"azure\",\"mintcream\"]);\nconst palette = convert([\"blue\", \"white\", \"green\", \"red\"]);\npalette.when = Date.now() + 1000;\ncontext.set(\"colors\", colors);\ncontext.set(\"palette\", palette);\n", "finalize": "", "libs": [ { "var": "Colors", "module": "colors" } ], "_mcu": { "mcu": false }, "x": 590, "y": 177, "wires": [ [ "993fb3df44b05e16" ] ] }, { "id": "a1be3b24a70a023f", "type": "function", "z": "a012fade3b5d57bb", "name": "wipe down", "func": "const state = context.get(\"state\");\nconst points = state.points;\n\nstate.y += 0.33;\nif (state.y > (100 + 30)) {\n state.y = 1;\n state.phase += 1;\n const c = Colors.getRGB(state.palette[state.phase % state.palette.length]);\n state.color = (c.r << 16) | (c.g << 8) | c.b\n}\nconst y = state.y;\nconst color = state.color;\nconst pixels = flow.get(\"pixels\");\nmsg.colors = pixels;\nfor (let i = 0, lightCount = state.lightCount; i < lightCount; i++) {\n const delta = y - points[i].y\n if (delta < 0)\n continue;\n if (delta > 20)\n pixels[i] = color;\n else if (Math.random() < 0.025)\n pixels[i] = color;\n}\n\nreturn msg;\n", "outputs": 1, "noerr": 0, "initialize": "const lightCount = 100;\nconst scale = 50;\nconst points = [];\nfor (let i = 0; i < lightCount; i++) {\n\tconst r = (Math.PI * 2) * i / lightCount;\n\tlet x = Math.cos(r);\n\tlet y = Math.sin(r);\n\n\tx = Math.round(x * scale) + scale;\n\ty = Math.round(y * scale) + scale;\n\tpoints.push({x, y /* , index: i */});\n}\n\ncontext.set(\"state\", {\n points,\n lightCount,\n y: 100,\n phase: 0,\n palette: [\"white\",\"blue\",\"red\",\"green\",\"yellow\",\"purple\"]\n});\n", "finalize": "", "libs": [ { "var": "Colors", "module": "colors" } ], "_mcu": { "mcu": false }, "x": 590, "y": 220, "wires": [ [ "993fb3df44b05e16" ] ] }, { "id": "1680623367985eb1", "type": "rpi-neopixels", "z": "a012fade3b5d57bb", "name": "100 Neopixels", "gpio": 18, "pixels": "100", "bgnd": "16,16,16", "fgnd": "blue", "wipe": "0", "mode": "shiftu", "rgb": "grb", "brightness": "100", "gamma": false, "_mcu": { "mcu": false }, "x": 898, "y": 226, "wires": [] }, { "id": "3302e770bc545890", "type": "complete", "z": "a012fade3b5d57bb", "name": "step animation", "scope": [ "1680623367985eb1", "8b161be95b2f7721" ], "uncaught": false, "_mcu": { "mcu": false }, "x": 118, "y": 226, "wires": [ [ "e6e9d646477d897a" ] ] }, { "id": "051616a3c9cae336", "type": "inject", "z": "a012fade3b5d57bb", "name": "timer", "props": [], "repeat": "60", "crontab": "", "once": false, "onceDelay": "0.01", "topic": "", "_mcu": { "mcu": false }, "x": 130, "y": 360, "wires": [ [ "304d2101548edd73" ] ] }, { "id": "304d2101548edd73", "type": "random", "z": "a012fade3b5d57bb", "name": "", "low": "0", "high": "2", "inte": "true", "property": "payload", "_mcu": { "mcu": false }, "x": 280, "y": 360, "wires": [ [ "4d93111f23e8d1af" ] ] }, { "id": "4d93111f23e8d1af", "type": "change", "z": "a012fade3b5d57bb", "name": "choose animation", "rules": [ { "t": "set", "p": "payload", "pt": "msg", "to": "NeopixelModes[msg.payload]", "tot": "flow" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "_mcu": { "mcu": false }, "x": 470, "y": 360, "wires": [ [ "4d8520215ecf3c89" ] ] }, { "id": "9bfa4623c08ca35e", "type": "comment", "z": "a012fade3b5d57bb", "name": "change animation every 60 seconds", "info": "", "_mcu": { "mcu": false }, "x": 160, "y": 314, "wires": [] }, { "id": "3e3f3aa04abeb829", "type": "comment", "z": "a012fade3b5d57bb", "name": "animate lights", "info": "", "_mcu": { "mcu": false }, "x": 90, "y": 160, "wires": [] }, { "id": "add3dc4b1f9798a8", "type": "function", "z": "a012fade3b5d57bb", "name": "color sparkle", "func": "const state = context.get(\"state\")\nconst pixels = flow.get(\"pixels\");\n\npixels.copyWithin(1, 0);\npixels[0] = state.colors[state.step++ % state.colors.length];\nmsg.colors = pixels;\n\nreturn msg;\n", "outputs": 1, "noerr": 0, "initialize": "const colors = [\n \"0,0,16\",\n \"0,0,32\",\n \"0,0,64\",\n \"0,0,96\",\n \"0,0,128\",\n \"0,0,160\",\n \"0,0,192\",\n \"0,0,224\",\n \"0,0,240\",\n \"0,0,255\",\n \"0,0,240\",\n \"0,0,224\",\n \"0,0,192\",\n \"0,0,160\",\n \"0,0,128\",\n \"0,0,96\",\n \"0,0,64\",\n \"0,0,32\",\n].map(c => {\n c = c.split(\",\");\n return (Number(c[0]) << 16) | (Number(c[1]) << 8) | Number(c[2]);\n});\n\ncontext.set(\"state\", {\n colors,\n step: 0\n});\n", "finalize": "", "libs": [], "_mcu": { "mcu": false }, "x": 590, "y": 260, "wires": [ [ "993fb3df44b05e16" ] ] }, { "id": "64682d54ae8459c3", "type": "comment", "z": "a012fade3b5d57bb", "name": "change animation on button press", "info": "", "_mcu": { "mcu": false }, "x": 160, "y": 434, "wires": [] }, { "id": "eb8999b84364c341", "type": "comment", "z": "a012fade3b5d57bb", "name": "change animation with MQTT message", "info": "", "_mcu": { "mcu": false }, "x": 170, "y": 554, "wires": [] }, { "id": "519460df2fb49336", "type": "comment", "z": "a012fade3b5d57bb", "name": "initialize", "info": "", "_mcu": { "mcu": false }, "x": 70, "y": 40, "wires": [] }, { "id": "e241931089c1d46b", "type": "inject", "z": "a012fade3b5d57bb", "name": "start-up", "props": [], "repeat": "", "crontab": "", "once": true, "onceDelay": "0", "topic": "", "_mcu": { "mcu": false }, "x": 140, "y": 80, "wires": [ [ "df6a43ea2958118d" ] ] }, { "id": "df6a43ea2958118d", "type": "change", "z": "a012fade3b5d57bb", "name": "animation", "rules": [ { "t": "set", "p": "NeopixelMode", "pt": "flow", "to": "random", "tot": "str" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "_mcu": { "mcu": false }, "x": 280, "y": 80, "wires": [ [ "9032e9fcb5e67e6e" ] ] }, { "id": "9032e9fcb5e67e6e", "type": "change", "z": "a012fade3b5d57bb", "name": "animation modes", "rules": [ { "t": "set", "p": "NeopixelModes", "pt": "flow", "to": "[\"wipe\",\"random\",\"sparkle\"]", "tot": "json" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "_mcu": { "mcu": false }, "x": 450, "y": 80, "wires": [ [ "bf803df994a1ea21" ] ] }, { "id": "c98462928a9ebe9a", "type": "function", "z": "a012fade3b5d57bb", "name": "off", "func": "const pixels = flow.get(\"pixels\")\n\npixels.fill(0);\nmsg.colors = pixels;\n\nreturn msg;\n", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "_mcu": { "mcu": false }, "x": 590, "y": 300, "wires": [ [ "993fb3df44b05e16" ] ] }, { "id": "6d66fe84cdb37819", "type": "rpi-gpio in", "z": "a012fade3b5d57bb", "name": "button", "pin": "0", "intype": "tri", "debounce": "25", "read": false, "bcm": true, "_mcu": { "mcu": false }, "x": 101, "y": 480, "wires": [ [ "c5e2e9691d8db411" ] ] }, { "id": "c5e2e9691d8db411", "type": "switch", "z": "a012fade3b5d57bb", "name": "button down?", "property": "payload", "propertyType": "msg", "rules": [ { "t": "eq", "v": "0", "vt": "num" } ], "checkall": "true", "repair": false, "outputs": 1, "_mcu": { "mcu": false }, "x": 269, "y": 480, "wires": [ [ "70e7a983d1ed05cb" ] ] }, { "id": "70e7a983d1ed05cb", "type": "function", "z": "a012fade3b5d57bb", "name": "next animation", "func": "let NeopixelModes = flow.get(\"NeopixelModes\");\nlet NeopixelMode = flow.get(\"NeopixelMode\");\nlet index = (NeopixelModes.indexOf(NeopixelMode) + 1) % NeopixelModes.length;\nmsg.payload = NeopixelModes[index];\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "_mcu": { "mcu": false }, "x": 460, "y": 480, "wires": [ [ "4d8520215ecf3c89" ] ] }, { "id": "4d8520215ecf3c89", "type": "change", "z": "a012fade3b5d57bb", "name": "set animation", "rules": [ { "t": "set", "p": "NeopixelMode", "pt": "flow", "to": "payload", "tot": "msg" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "_mcu": { "mcu": false }, "x": 700, "y": 480, "wires": [ [ "38883a439d434e96" ] ] }, { "id": "38883a439d434e96", "type": "mqtt out", "z": "a012fade3b5d57bb", "name": "publish animation", "topic": "moddable/wreath/status/animation", "qos": "0", "retain": "", "respTopic": "", "contentType": "", "userProps": "", "correl": "", "expiry": "", "broker": "40068d0db5e8893c", "_mcu": { "mcu": false }, "x": 930, "y": 480, "wires": [] }, { "id": "6308a1e76a85998c", "type": "mqtt in", "z": "a012fade3b5d57bb", "name": "receive animation", "topic": "moddable/wreath/set/animation", "qos": "0", "datatype": "auto-detect", "broker": "40068d0db5e8893c", "nl": false, "rap": true, "rh": 0, "inputs": 0, "_mcu": { "mcu": false }, "x": 140, "y": 600, "wires": [ [ "55b2ce6c5f356fdb" ] ] }, { "id": "52542f7625238930", "type": "audioout", "z": "a012fade3b5d57bb", "name": "stream song", "volume": "1.5", "_mcu": { "mcu": false }, "x": 850, "y": 800, "wires": [] }, { "id": "6e6c088232a4230a", "type": "comment", "z": "a012fade3b5d57bb", "name": "stream song", "info": "", "_mcu": { "mcu": false }, "x": 90, "y": 680, "wires": [] }, { "id": "e2fdebe581e407e3", "type": "complete", "z": "a012fade3b5d57bb", "name": "next song", "scope": [ "52542f7625238930", "8b161be95b2f7721" ], "uncaught": false, "_mcu": { "mcu": false }, "x": 112, "y": 730, "wires": [ [ "69d0727397f739f5" ] ] }, { "id": "fe9cde216a0f89c3", "type": "random", "z": "a012fade3b5d57bb", "name": "", "low": "0", "high": "9", "inte": "true", "property": "payload", "_mcu": { "mcu": false }, "x": 455, "y": 731, "wires": [ [ "3090e36098d2ae31" ] ] }, { "id": "3090e36098d2ae31", "type": "change", "z": "a012fade3b5d57bb", "name": "map song", "rules": [ { "t": "set", "p": "wave", "pt": "msg", "to": "Songs[msg.payload]", "tot": "flow" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "_mcu": { "mcu": false }, "x": 595, "y": 731, "wires": [ [ "60a995290b2fe0a3" ] ] }, { "id": "bf803df994a1ea21", "type": "change", "z": "a012fade3b5d57bb", "name": "song list", "rules": [ { "t": "set", "p": "Songs", "pt": "flow", "to": "[\"http://test.moddable.com/audio/ChristmasMusic/deck_halls.wav\",\"http://test.moddable.com/audio/ChristmasMusic/god_rest.wav\",\"http://test.moddable.com/audio/ChristmasMusic/hark.wav\",\"http://test.moddable.com/audio/ChristmasMusic/jesu.wav\",\"http://test.moddable.com/audio/ChristmasMusic/jingle.wav\",\"http://test.moddable.com/audio/ChristmasMusic/jingle.wav\",\"http://test.moddable.com/audio/ChristmasMusic/joy.wav\",\"http://test.moddable.com/audio/ChristmasMusic/norad.wav\",\"http://test.moddable.com/audio/ChristmasMusic/silent.wav\",\"http://test.moddable.com/audio/ChristmasMusic/wish_merry.wav\"]", "tot": "json" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "_mcu": { "mcu": false }, "x": 620, "y": 80, "wires": [ [ "0dc1b8193c78b5bf" ] ] }, { "id": "2c276fe9386da0c3", "type": "mqtt out", "z": "a012fade3b5d57bb", "name": "publish song", "topic": "moddable/wreath/status/song", "qos": "0", "retain": "", "respTopic": "", "contentType": "", "userProps": "", "correl": "", "expiry": "", "broker": "40068d0db5e8893c", "_mcu": { "mcu": false }, "x": 1045, "y": 731, "wires": [] }, { "id": "64ac0ae224ff9500", "type": "change", "z": "a012fade3b5d57bb", "name": "", "rules": [ { "t": "move", "p": "wave", "pt": "msg", "to": "payload", "tot": "msg" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "_mcu": { "mcu": false }, "x": 855, "y": 731, "wires": [ [ "2c276fe9386da0c3" ] ] }, { "id": "c657fa0f1c0ace9f", "type": "mqtt in", "z": "a012fade3b5d57bb", "name": "receive song request", "topic": "moddable/wreath/set/song", "qos": "0", "datatype": "auto-detect", "broker": "40068d0db5e8893c", "nl": false, "rap": true, "rh": 0, "inputs": 0, "_mcu": { "mcu": false }, "x": 141, "y": 868, "wires": [ [ "84199ada332f7f13" ] ] }, { "id": "84199ada332f7f13", "type": "change", "z": "a012fade3b5d57bb", "name": "set requested song", "rules": [ { "t": "move", "p": "payload", "pt": "msg", "to": "wave", "tot": "msg" }, { "t": "set", "p": "requestedSong", "pt": "flow", "to": "true", "tot": "bool" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "_mcu": { "mcu": false }, "x": 496, "y": 868, "wires": [ [ "60a995290b2fe0a3" ] ] }, { "id": "69d0727397f739f5", "type": "switch", "z": "a012fade3b5d57bb", "name": "", "property": "requestedSong", "propertyType": "flow", "rules": [ { "t": "null" }, { "t": "else" } ], "checkall": "true", "repair": false, "outputs": 2, "_mcu": { "mcu": false }, "x": 312, "y": 755, "wires": [ [ "fe9cde216a0f89c3" ], [ "90fe2d2c33022e59" ] ] }, { "id": "90fe2d2c33022e59", "type": "change", "z": "a012fade3b5d57bb", "name": "clear requested", "rules": [ { "t": "delete", "p": "requestedSong", "pt": "flow" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "_mcu": { "mcu": false }, "x": 475, "y": 786, "wires": [ [] ] }, { "id": "a5edc4b251a33ed8", "type": "catch", "z": "a012fade3b5d57bb", "name": "streaming error", "scope": [ "52542f7625238930" ], "uncaught": false, "_mcu": { "mcu": false }, "x": 131, "y": 788, "wires": [ [ "69d0727397f739f5" ] ] }, { "id": "0dc1b8193c78b5bf", "type": "function", "z": "a012fade3b5d57bb", "name": "pixels", "func": "const pixels = new Array(100);\npixels.fill(0);\nflow.set(\"pixels\", pixels);\n\nreturn msg;\n", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "_mcu": { "mcu": false }, "x": 750, "y": 80, "wires": [ [ "8b161be95b2f7721" ] ] }, { "id": "8b161be95b2f7721", "type": "debug", "z": "a012fade3b5d57bb", "name": "initialized", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "statusVal": "", "statusType": "auto", "_mcu": { "mcu": false }, "x": 880, "y": 80, "wires": [] }, { "id": "40068d0db5e8893c", "type": "mqtt-broker", "name": "test.mosquitto.org", "broker": "test.mosquitto.org", "port": "1883", "clientid": "", "autoConnect": true, "usetls": false, "protocolVersion": "4", "keepalive": "60", "cleansession": true, "birthTopic": "", "birthQos": "0", "birthPayload": "", "birthMsg": {}, "closeTopic": "", "closeQos": "0", "closePayload": "", "closeMsg": {}, "willTopic": "", "willQos": "0", "willPayload": "", "willMsg": {}, "userProps": "", "sessionExpiry": "", "credentials": {} } ]Check out the Moddable blog for a post introducing the technologies used in the project, including the ESP32-S3 microcontroller, streaming audio, Neopixels, MQTT, and, of course, Node-RED MCU Edition.
I'm still something of a rookie at using Node-RED. I learned a lot building the flows for this project and discovered some techniques that work really well.I thought some of what I've learned might be helpful to others getting started with Node-RED and microcontrollers. I made a video that walks through the flow, node-by-node, explaining how they fit together to make the wreath work. It is a little long (sorry!) but hopefully helpful.
Beta Was this translation helpful? Give feedback.
All reactions