├── LICENSE ├── assets ├── change-configure.png ├── http-request-configure.png ├── inject-configure.png └── node-red-mcu-logo.png ├── examples ├── dashboard │ ├── switch │ │ ├── assets │ │ │ ├── brightness-bar.png │ │ │ ├── brightness-icon.png │ │ │ ├── color-thumb.png │ │ │ ├── color-wheel.png │ │ │ ├── power-bar.png │ │ │ └── power-thumb.png │ │ ├── flows.json │ │ ├── main.js │ │ ├── manifest.json │ │ └── templates.js │ ├── uitest │ │ ├── flows.json │ │ ├── main.js │ │ └── manifest.json │ └── weather │ │ ├── assets │ │ ├── Roboto-Regular-24.fnt │ │ ├── Roboto-Regular-24.png │ │ ├── Temperature-Bold-48.fnt │ │ ├── Temperature-Bold-48.png │ │ ├── Weather-Regular-120.fnt │ │ └── Weather-Regular-120.png │ │ ├── flows.json │ │ ├── main.js │ │ ├── manifest.json │ │ └── templates.js └── lower-case │ ├── lower-case.html │ ├── lower-case.js │ ├── manifest.json │ └── package.json ├── flows.json ├── main.js ├── manifest.json ├── manifest_core.json ├── manifest_host.json ├── manifest_runtime.json ├── manifest_ui.json ├── mods ├── host │ ├── main.js │ └── manifest.json ├── mod │ └── manifest.json └── readme.md ├── node_types.json ├── nodered.c ├── nodered.js ├── nodes ├── audioout │ ├── audioout.js │ ├── manifest.json │ ├── mcu_audioout.html │ ├── mcu_audioout.js │ └── package.json ├── dashboard │ ├── ScrollerBehaviors.js │ ├── assets │ │ ├── Roboto-Medium-12.fnt │ │ ├── Roboto-Medium-12.png │ │ ├── Roboto-Medium-18.fnt │ │ ├── Roboto-Medium-18.png │ │ ├── Roboto-Regular-18.fnt │ │ ├── Roboto-Regular-18.png │ │ ├── button.png │ │ ├── glyphs.png │ │ ├── popup.png │ │ ├── slider.png │ │ ├── switch.png │ │ ├── ui_colour_picker.png │ │ └── ui_colour_picker_mask.png │ ├── manifest.json │ ├── ui_chart.js │ ├── ui_colour_picker.js │ ├── ui_gauge.js │ ├── ui_nodes.js │ ├── ui_templates.js │ └── ui_text_input.js ├── function │ ├── delay │ │ ├── 89-delay.js │ │ └── manifest.json │ ├── random │ │ ├── manifest.json │ │ └── random.js │ ├── template │ │ ├── manifest.json │ │ ├── mustache.js │ │ └── template.js │ └── trigger │ │ ├── 89-trigger.js │ │ └── manifest.json ├── mcu │ ├── analog │ │ ├── analog.js │ │ ├── manifest.json │ │ ├── mcu_analog.html │ │ └── mcu_analog.js │ ├── assets │ │ ├── ediit=sensor-node.png │ │ ├── edit-sensor-node-host-provider.png │ │ ├── flow.png │ │ └── palette.png │ ├── clock │ │ ├── clock.js │ │ ├── manifest.json │ │ ├── mcu_clock.html │ │ └── mcu_clock.js │ ├── digital │ │ ├── digital.js │ │ ├── manifest.json │ │ ├── mcu_digital.html │ │ └── mcu_digital.js │ ├── i2c │ │ ├── i2c.js │ │ ├── manifest.json │ │ ├── mcu_i2c.html │ │ └── mcu_i2c.js │ ├── neopixels │ │ ├── colors.js │ │ ├── manifest.json │ │ ├── mcu_neopixels.html │ │ ├── mcu_neopixels.js │ │ └── neopixels.js │ ├── package.json │ ├── pulsecount │ │ ├── manifest.json │ │ ├── mcu_pulsecount.html │ │ ├── mcu_pulsecount.js │ │ └── pulsecount.js │ ├── pulsewidth │ │ ├── manifest.json │ │ ├── mcu_pulsewidth.html │ │ ├── mcu_pulsewidth.js │ │ └── pulsewidth.js │ ├── pwm │ │ ├── manifest.json │ │ ├── mcu_pwm.html │ │ ├── mcu_pwm.js │ │ └── pwm.js │ ├── readme.md │ ├── resources │ │ ├── database.json │ │ └── library.js │ └── sensor │ │ ├── icons │ │ └── mcu.png │ │ ├── manifest.json │ │ ├── mcu_sensor.html │ │ ├── mcu_sensor.js │ │ └── sensor.js ├── network │ ├── httprequest │ │ ├── httprequest.js │ │ └── manifest.json │ ├── httpserver │ │ ├── httpserver.js │ │ └── manifest.json │ ├── mqtt │ │ ├── manifest.json │ │ └── mqttnodes.js │ ├── tcp │ │ ├── manifest.json │ │ └── tcpnodes.js │ ├── tls-config │ │ ├── manifest.json │ │ └── tls-config.js │ ├── udp │ │ ├── manifest.json │ │ └── udpnodes.js │ └── websocketnodes │ │ ├── manifest.json │ │ └── websocketnodes.js ├── ota-update │ ├── examples │ │ ├── ota-pull.json │ │ └── ota-push.json │ ├── manifest.json │ ├── mcu_ota-update.html │ ├── mcu_ota-update.js │ ├── ota-update.js │ ├── package.json │ └── readme.md ├── parser │ └── csv │ │ ├── 70-CSV.js │ │ └── manifest.json ├── restart │ ├── examples │ │ └── example.json │ ├── manifest.json │ ├── mcu_restart.html │ ├── mcu_restart.js │ ├── package.json │ ├── readme.md │ ├── restart.c │ └── restart.js ├── rpi │ ├── rpi-ds18b20 │ │ ├── manifest.json │ │ └── rpi-ds18b20.js │ ├── rpi-gpio │ │ ├── manifest.json │ │ └── rpi-gpio.js │ └── rpi-neopixels │ │ ├── manifest.json │ │ └── rpi-neopixels.js ├── sequence │ ├── batch │ │ ├── 19-batch.js │ │ └── manifest.json │ ├── join │ │ ├── 17-join.js │ │ └── manifest.json │ ├── sort │ │ ├── 18-sort.js │ │ └── manifest.json │ └── split │ │ ├── manifest.json │ │ └── split.js ├── storage │ └── file │ │ ├── manifest.json │ │ └── node-red-files.js └── weather │ └── openweathermap │ ├── manifest.json │ └── weather.js ├── readme.md ├── setuptimezone.js ├── setupwifi.js └── tools └── xsbugproxy ├── index.js └── package.json /assets/change-configure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/assets/change-configure.png -------------------------------------------------------------------------------- /assets/http-request-configure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/assets/http-request-configure.png -------------------------------------------------------------------------------- /assets/inject-configure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/assets/inject-configure.png -------------------------------------------------------------------------------- /assets/node-red-mcu-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/assets/node-red-mcu-logo.png -------------------------------------------------------------------------------- /examples/dashboard/switch/assets/brightness-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/examples/dashboard/switch/assets/brightness-bar.png -------------------------------------------------------------------------------- /examples/dashboard/switch/assets/brightness-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/examples/dashboard/switch/assets/brightness-icon.png -------------------------------------------------------------------------------- /examples/dashboard/switch/assets/color-thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/examples/dashboard/switch/assets/color-thumb.png -------------------------------------------------------------------------------- /examples/dashboard/switch/assets/color-wheel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/examples/dashboard/switch/assets/color-wheel.png -------------------------------------------------------------------------------- /examples/dashboard/switch/assets/power-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/examples/dashboard/switch/assets/power-bar.png -------------------------------------------------------------------------------- /examples/dashboard/switch/assets/power-thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/examples/dashboard/switch/assets/power-thumb.png -------------------------------------------------------------------------------- /examples/dashboard/switch/main.js: -------------------------------------------------------------------------------- 1 | import "nodered"; 2 | import builder from "flows"; 3 | import buildModel from "./ui_nodes"; 4 | import { REDApplication } from "./ui_templates"; 5 | 6 | export default function() { 7 | RED.build(builder); 8 | const model = buildModel(); 9 | 10 | return new REDApplication(model, { commandListLength:1024, displayListLength:2048, touchCount:1, pixels: 240 * 2 }); 11 | } 12 | -------------------------------------------------------------------------------- /examples/dashboard/switch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_base.json", 4 | "$(MODULES)/files/preference/manifest.json", 5 | "../../../nodes/dashboard/manifest.json" 6 | ], 7 | "creation": { 8 | "keys": { 9 | "available": 128 10 | }, 11 | "stack": 768 12 | }, 13 | "modules": { 14 | "*": [ 15 | "./main", 16 | "./templates", 17 | { 18 | "source": "./flows", 19 | "transform": "nodered2mcu" 20 | } 21 | ] 22 | }, 23 | "preload": [ 24 | "templates", 25 | "flows" 26 | ], 27 | "resources":{ 28 | "*": [ 29 | "./assets/color-wheel" 30 | ], 31 | "*-mask": [ 32 | "./assets/brightness-bar", 33 | "./assets/brightness-icon", 34 | "./assets/color-thumb", 35 | "./assets/power-bar", 36 | "./assets/power-thumb" 37 | ] 38 | }, 39 | "platforms": { 40 | "esp32": { 41 | "creation": { 42 | "static": 0, 43 | "chunk": { 44 | "initial": 81920, 45 | "incremental": 0 46 | }, 47 | "heap": { 48 | "initial": 5120, 49 | "incremental": 0 50 | } 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/dashboard/switch/templates.js: -------------------------------------------------------------------------------- 1 | import {} from "piu/MC"; 2 | import { 3 | registerTemplate 4 | } from "ui_nodes"; 5 | import { 6 | REDBehavior, 7 | REDSliderBehavior, 8 | REDSwitchBehavior, 9 | } from "ui_templates"; 10 | 11 | const BLACK = "black"; 12 | const LIGHT_GRAY = "#a9a9a9"; 13 | const WHITE = "white"; 14 | const GREEN = "#57ad62" 15 | const TRANSPARENT = "transparent"; 16 | 17 | const theme = Object.freeze({ 18 | skins: { 19 | brightnessBar: { texture:{ path:"brightness-bar.png" }, color:[LIGHT_GRAY, WHITE], x:0, y:0, width:64, height:60, left:16, right:16 }, 20 | brightnessIcon: { texture:{ path:"brightness-icon.png" }, color: [WHITE, BLACK], x:0, y:0, width:40, height:40 }, 21 | colorWheel: { texture:{ path:"color-wheel.png" }, x:0, y:0, width:160, height:160 }, 22 | colorThumb: { texture:{ path:"color-thumb.png" }, color:["#333333", WHITE], x:0, y:0, width:40, height:40, variants:40 }, 23 | powerBar: { texture:{ path:"power-bar.png" }, color:[TRANSPARENT, LIGHT_GRAY, GREEN], x:0, y:0, width:120, height:60 }, 24 | powerThumb: { texture:{ path:"power-thumb.png" }, color:WHITE, x:0, y:0, width:60, height:60 }, 25 | } 26 | }, true); 27 | 28 | 29 | let BrightnessSlider = Container.template($ => ({ 30 | left:$.left, width:$.width, top:$.top, height:$.height, 31 | contents: [ 32 | Container($, { 33 | left:20, right:20, height:60, skin:theme.skins.brightnessBar, active:true, Behavior:REDSliderBehavior, 34 | contents: [ 35 | Content($, { skin:theme.skins.brightnessIcon }), 36 | Container($, { 37 | left:0, width:0, top:0, bottom:0, clip:true, 38 | contents: [ 39 | Content($, { left:0, width:$.width - 40, top:0, bottom:0, skin:theme.skins.brightnessBar, state:1 }), 40 | Content($, { left:($.width - 80) >> 1, skin:theme.skins.brightnessIcon, state:1 }), 41 | ], 42 | }), 43 | Content($, { }), 44 | ], 45 | }), 46 | ], 47 | })); 48 | registerTemplate("piu-slider-brightness", BrightnessSlider); 49 | 50 | class ColorWheelBehavior extends REDBehavior { 51 | onTouchBegan(container, id, x, y, ticks) { 52 | container.captureTouch(id, x, y, ticks); 53 | this.onTouchMoved(container, id, x, y, ticks); 54 | } 55 | onTouchMoved(container, id, x, y, ticks) { 56 | const data = this.data; 57 | const thumb = container.last; 58 | const thumbRadius = thumb.width >> 1; 59 | const wheelRadius = container.width >> 1; 60 | const r = wheelRadius - thumbRadius; 61 | const cx = container.x + wheelRadius; 62 | const cy = container.y + wheelRadius; 63 | x -= cx; 64 | y -= cy; 65 | const a = Math.atan2(y, x); 66 | let d = Math.sqrt(x ** 2 + y ** 2); 67 | if (d > r) { 68 | x = Math.cos(a) * r; 69 | y = Math.sin(a) * r; 70 | d = r; 71 | } 72 | thumb.x = cx + x - thumbRadius; 73 | thumb.y = cy + y - thumbRadius; 74 | data.hsv = { h:-a * 180 / Math.PI, s:d / r, v:1 }; 75 | if (data.dynOutput) 76 | data.onChanged(); 77 | thumb.first.invalidate(); 78 | } 79 | onTouchEnded(container, id, x, y, ticks) { 80 | this.data.onChanged(); 81 | } 82 | onUpdate(container) { 83 | const thumb = container.last; 84 | const thumbRadius = thumb.width >> 1; 85 | const wheelRadius = container.width >> 1; 86 | const r = wheelRadius - thumbRadius; 87 | const cx = container.x + wheelRadius; 88 | const cy = container.y + wheelRadius; 89 | const hsv = this.data.hsv; 90 | const a = -hsv.h * Math.PI / 180; 91 | const d = hsv.s * r; 92 | const x = Math.cos(a) * d; 93 | const y = Math.sin(a) * d; 94 | thumb.x = cx + x - thumbRadius; 95 | thumb.y = cy + y - thumbRadius; 96 | thumb.first.invalidate(); 97 | } 98 | } 99 | 100 | const ColorWheel = Container.template($ => ({ 101 | left:$.left, width:$.width, top:$.top, height:$.height, clip:true, 102 | contents: [ 103 | Container($, { 104 | width:160, height:160, active:true, Behavior:ColorWheelBehavior, 105 | contents: [ 106 | Content($, { skin:theme.skins.colorWheel }), 107 | Container($, { 108 | left:60, width:40, top:60, height:40, 109 | contents: [ 110 | Port($, { 111 | width:40, height:40, 112 | Behavior: class extends Behavior { 113 | onCreate(port, data) { 114 | this.data = data; 115 | this.texture = new Texture(theme.skins.colorThumb.texture); 116 | } 117 | onDraw(port) { 118 | const color = this.data.color; 119 | port.drawTexture(this.texture, rgb(color.r, color.g, color.b), 0, 0, 0, 0, 40, 40); 120 | } 121 | }, 122 | }), 123 | Content($, { skin:theme.skins.colorThumb, variant:1 }), 124 | ], 125 | }), 126 | ], 127 | }), 128 | ], 129 | })); 130 | registerTemplate("piu-color-wheel", ColorWheel); 131 | 132 | const PowerSwitch = Container.template($ => ({ 133 | left:$.left, width:$.width, top:$.top, height:$.height, clip:true, 134 | contents: [ 135 | Container($, { 136 | width:120, active:true, Behavior:REDSwitchBehavior, 137 | contents: [ 138 | Content($, { width:120, skin:theme.skins.powerBar }), 139 | Content($, { left:0, width:60, top:0, skin:theme.skins.powerThumb }), 140 | ], 141 | }), 142 | ], 143 | })); 144 | registerTemplate("piu-switch-power", PowerSwitch); 145 | -------------------------------------------------------------------------------- /examples/dashboard/uitest/main.js: -------------------------------------------------------------------------------- 1 | import "nodered"; 2 | import builder from "flows"; 3 | import buildModel from "./ui_nodes"; 4 | import { REDApplication } from "./ui_templates"; 5 | 6 | RED.build(builder); 7 | const model = buildModel(); 8 | 9 | export default new REDApplication(model, { commandListLength:4096, displayListLength:+4096, touchCount:1, pixels: 240 * 48 }); 10 | -------------------------------------------------------------------------------- /examples/dashboard/uitest/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_base.json", 4 | "$(MODULES)/files/preference/manifest.json", 5 | "../../../nodes/dashboard/manifest.json", 6 | "../../../nodes/function/random/manifest.json", 7 | "../../../nodes/function/trigger/manifest.json" 8 | ], 9 | "creation": { 10 | "keys": { 11 | "available": 128 12 | }, 13 | "stack": 1024 14 | }, 15 | "config": { 16 | "ui_text_input": true 17 | }, 18 | "modules": { 19 | "*": [ 20 | "./main", 21 | { 22 | "source": "./flows", 23 | "transform": "nodered2mcu" 24 | } 25 | ] 26 | }, 27 | "preload": [ 28 | "flows" 29 | ], 30 | "platforms": { 31 | "esp32": { 32 | "creation": { 33 | "static": 0, 34 | "chunk": { 35 | "initial": 65536, 36 | "incremental": 0 37 | }, 38 | "heap": { 39 | "initial": 4096, 40 | "incremental": 0 41 | }, 42 | "keys": { 43 | "available": 128 44 | }, 45 | "stack": 600 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/dashboard/weather/assets/Roboto-Regular-24.fnt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/examples/dashboard/weather/assets/Roboto-Regular-24.fnt -------------------------------------------------------------------------------- /examples/dashboard/weather/assets/Roboto-Regular-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/examples/dashboard/weather/assets/Roboto-Regular-24.png -------------------------------------------------------------------------------- /examples/dashboard/weather/assets/Temperature-Bold-48.fnt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/examples/dashboard/weather/assets/Temperature-Bold-48.fnt -------------------------------------------------------------------------------- /examples/dashboard/weather/assets/Temperature-Bold-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/examples/dashboard/weather/assets/Temperature-Bold-48.png -------------------------------------------------------------------------------- /examples/dashboard/weather/assets/Weather-Regular-120.fnt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/examples/dashboard/weather/assets/Weather-Regular-120.fnt -------------------------------------------------------------------------------- /examples/dashboard/weather/assets/Weather-Regular-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/examples/dashboard/weather/assets/Weather-Regular-120.png -------------------------------------------------------------------------------- /examples/dashboard/weather/main.js: -------------------------------------------------------------------------------- 1 | import "nodered"; 2 | import builder from "flows"; 3 | import buildModel from "./ui_nodes"; 4 | import { REDApplication } from "./ui_templates"; 5 | 6 | export default function() { 7 | RED.build(builder); 8 | const model = buildModel(); 9 | 10 | return new REDApplication(model, { commandListLength:2048, displayListLength:4096, touchCount:1, pixels: 240 * 8 }); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /examples/dashboard/weather/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_base.json", 4 | "$(MODULES)/files/preference/manifest.json", 5 | "$(MODDABLE)/modules/io/manifest.json", 6 | "../../../nodes/dashboard/manifest.json", 7 | "../../../nodes/weather/openweathermap/manifest.json" 8 | ], 9 | "creation": { 10 | "static": 0, 11 | "chunk": { 12 | "initial": 81920, 13 | "incremental": 0 14 | }, 15 | "heap": { 16 | "initial": 5120, 17 | "incremental": 0 18 | }, 19 | "stack": 1024, 20 | "keys": { 21 | "available": 128 22 | } 23 | }, 24 | "modules": { 25 | "*": [ 26 | "./main", 27 | "./templates", 28 | { 29 | "source": "./flows", 30 | "transform": "nodered2mcu" 31 | } 32 | ] 33 | }, 34 | "preload": [ 35 | "templates", 36 | "flows" 37 | ], 38 | "resources":{ 39 | "*-mask": [ 40 | "./assets/Roboto-Regular-24", 41 | "./assets/Temperature-Bold-48", 42 | "./assets/Weather-Regular-120" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/dashboard/weather/templates.js: -------------------------------------------------------------------------------- 1 | import {} from "piu/MC"; 2 | import { 3 | registerTemplate 4 | } from "ui_nodes"; 5 | import { 6 | REDBehavior, 7 | REDTextBehavior, 8 | } from "ui_templates"; 9 | 10 | 11 | 12 | const BLACK = "black"; 13 | const LIGHT_GRAY = "#a9a9a9"; 14 | const WHITE = "white"; 15 | const GREEN = "#57ad62" 16 | const TRANSPARENT = "transparent"; 17 | 18 | const theme = Object.freeze({ 19 | styles: { 20 | text: { font:"24px Roboto", color:WHITE }, 21 | temperature: { font:"bold 48px Temperature", color:WHITE }, 22 | weather: { font:"120px Weather", color:WHITE }, 23 | }, 24 | weatherCharacters: { 25 | "wi-owm-01d": "\uf00d", 26 | "wi-owm-02d": "\uf00c", 27 | "wi-owm-03d": "\uf002", 28 | "wi-owm-04d": "\uf013", 29 | "wi-owm-09d": "\uf017", 30 | "wi-owm-10d": "\uf019", 31 | "wi-owm-11d": "\uf01e", 32 | "wi-owm-13d": "\uf01b", 33 | "wi-owm-50d": "\uf014", 34 | "wi-owm-01n": "\uf02e", 35 | "wi-owm-02n": "\uf081", 36 | "wi-owm-03n": "\uf07e", 37 | "wi-owm-04n": "\uf086", 38 | "wi-owm-09n": "\uf026", 39 | "wi-owm-10n": "\uf028", 40 | "wi-owm-11n": "\uf02c", 41 | "wi-owm-13n": "\uf02a", 42 | "wi-owm-50n": "\uf04a", 43 | } 44 | }, true); 45 | 46 | let CityText = Label.template($ => ({ 47 | anchor:"VALUE", left:$.left, width:$.width, top:$.top, height:$.height, style:theme.styles.text, 48 | Behavior: REDTextBehavior, 49 | })); 50 | registerTemplate("piu-text-city", CityText); 51 | 52 | let TemperatureText = Label.template($ => ({ 53 | anchor:"VALUE", left:$.left, width:$.width, top:$.top, height:$.height, style:theme.styles.temperature, 54 | Behavior: REDTextBehavior, 55 | })); 56 | registerTemplate("piu-text-temperature", TemperatureText); 57 | 58 | let WeatherTemplate = Label.template($ => ({ 59 | left:$.left, width:$.width, top:$.top, height:$.height, style:theme.styles.weather, 60 | Behavior: class extends REDBehavior { 61 | onUpdate(label) { 62 | const string = theme.weatherCharacters[this.data.payload]; 63 | if (string) 64 | label.string = string; 65 | } 66 | } 67 | })); 68 | registerTemplate("piu-template-weather", WeatherTemplate); 69 | 70 | let WeatherText = Label.template($ => ({ 71 | anchor:"VALUE", left:$.left, width:$.width, top:$.top, height:$.height, style:theme.styles.text, 72 | Behavior: REDTextBehavior, 73 | })); 74 | registerTemplate("piu-text-weather", WeatherText); 75 | -------------------------------------------------------------------------------- /examples/lower-case/lower-case.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 23 | 24 | 27 | -------------------------------------------------------------------------------- /examples/lower-case/lower-case.js: -------------------------------------------------------------------------------- 1 | module.exports = function(RED) { 2 | function LowerCaseNode(config) { 3 | RED.nodes.createNode(this,config); 4 | var node = this; 5 | node.on('input', function(msg) { 6 | msg.payload = msg.payload.toLowerCase(); 7 | node.send(msg); 8 | }); 9 | } 10 | RED.nodes.registerType("lower-case",LowerCaseNode); 11 | } 12 | -------------------------------------------------------------------------------- /examples/lower-case/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "*": "./lower-case" 4 | }, 5 | "preload": "lower-case" 6 | } 7 | -------------------------------------------------------------------------------- /examples/lower-case/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@moddable-node-red/example-lower-case", 3 | "version": "1.0.0", 4 | "description": "example", 5 | "license": "UNLICENSED", 6 | "node-red" : { 7 | "nodes": { 8 | "lower-case": "lower-case.js" 9 | } 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /flows.json: -------------------------------------------------------------------------------- 1 | [ 2 | ] 3 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | import "nodered"; // import for global side effects 2 | import Modules from "modules"; 3 | import Timer from "timer"; 4 | 5 | if (!Modules.has("flows")) 6 | trace("no flows installed\n"); 7 | else { 8 | Timer.set(function() { // run on an empty stack 9 | const flows = Modules.importNow("flows"); 10 | RED.build(flows); 11 | 12 | if (globalThis.REDTheme) { 13 | if (!Modules.has("ui_nodes") || !Modules.has("ui_templates")) 14 | trace("flow neeeds UI nodes; not build into host \n"); 15 | else { 16 | const buildModel = Modules.importNow("ui_nodes"); 17 | const templates = Modules.importNow("ui_templates"); 18 | const REDApplication = templates.REDApplication; 19 | if (REDApplication) { 20 | try { 21 | const model = buildModel(); 22 | new REDApplication(model, { commandListLength:4096, displayListLength:8192, touchCount:1, pixels: 240 * 48 }); 23 | } 24 | catch { 25 | } 26 | } 27 | } 28 | } 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./manifest_host.json" 4 | ], 5 | "modules": { 6 | "*": [ 7 | "./main", 8 | { 9 | "source": "./flows", 10 | "transform": "nodered2mcu" 11 | } 12 | ] 13 | }, 14 | "preload": [ 15 | "flows" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /manifest_core.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_base.json", 4 | "$(MODDABLE)/modules/base/deepEqual/manifest.json", 5 | "$(MODDABLE)/modules/base/structuredClone/manifest.json", 6 | "$(MODDABLE)/modules/base/modules/manifest.json", 7 | "$(MODDABLE)/modules/data/hex/manifest.json", 8 | "$(MODDABLE)/modules/io/manifest.json", 9 | "$(MODDABLE)/modules/files/preference/manifest.json" 10 | ], 11 | "modules": { 12 | "*": [ 13 | "./nodered" 14 | ] 15 | }, 16 | "preload": [ 17 | "nodered" 18 | ], 19 | "defines": { 20 | "xs": { 21 | "xsbug_hooks": 1 22 | } 23 | }, 24 | "config": { 25 | "noderedmcu": { 26 | }, 27 | "sntp": "pool.ntp.org" 28 | }, 29 | "platforms": { 30 | "win": { 31 | "error": "Node-RED MCU does not currently run on Windows simulator. You can still build for device targets." 32 | }, 33 | "esp": { 34 | "modules": { 35 | "~": [ 36 | "$(BUILD)/devices/esp/setup/network" 37 | ], 38 | "setup/network": "./setupwifi", 39 | "setup/timezone": "./setuptimezone", 40 | "wifi/connection": "$(MODDABLE)/examples/network/wifi/wificonnection/wificonnection" 41 | }, 42 | "preload": [ 43 | "setup/timezone", 44 | "wifi/connection" 45 | ] 46 | }, 47 | "esp32": { 48 | "modules": { 49 | "~": [ 50 | "$(BUILD)/devices/esp32/setup/network" 51 | ], 52 | "setup/network": "./setupwifi", 53 | "setup/timezone": "./setuptimezone", 54 | "wifi/connection": "$(MODDABLE)/examples/network/wifi/wificonnection/wificonnection" 55 | }, 56 | "preload": [ 57 | "setup/timezone", 58 | "wifi/connection" 59 | ] 60 | }, 61 | "pico": { 62 | "modules": { 63 | "~": [ 64 | "$(BUILD)/devices/pico/setup/network" 65 | ], 66 | "setup/network": "./setupwifi", 67 | "setup/timezone": "./setuptimezone", 68 | "wifi/connection": "$(MODDABLE)/examples/network/wifi/wificonnection/wificonnection" 69 | }, 70 | "preload": [ 71 | "setup/timezone", 72 | "wifi/connection" 73 | ] 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /manifest_host.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./manifest_runtime.json" 4 | ], 5 | "creation": { 6 | "keys": { 7 | "available": 128 8 | }, 9 | "stack": 768 10 | }, 11 | "platforms": { 12 | "esp": { 13 | "creation": { 14 | "static": 37888, 15 | "stack": 384 16 | } 17 | }, 18 | "esp32": { 19 | "creation": { 20 | "static": 0, 21 | "chunk": { 22 | "initial": 81920, 23 | "incremental": 0 24 | }, 25 | "heap": { 26 | "initial": 5120, 27 | "incremental": 0 28 | }, 29 | "keys": { 30 | "available": 512 31 | } 32 | } 33 | }, 34 | "pico": { 35 | "creation": { 36 | "static": 131072 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /manifest_runtime.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "NODEREDMCU": "./" 4 | }, 5 | "include": [ 6 | "./manifest_core.json" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /manifest_ui.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "UINODES": "./nodes/dashboard", 4 | "UIASSETS": "./nodes/dashboard/assets" 5 | }, 6 | "include": [ 7 | "$(MODDABLE)/examples/manifest_piu.json", 8 | "$(MODDABLE)/modules/input/expanding-keyboard/vertical/manifest.json" 9 | ], 10 | "modules": { 11 | "*": [ 12 | "$(UINODES)/ScrollerBehaviors", 13 | "$(UINODES)/ui_nodes", 14 | "$(UINODES)/ui_templates", 15 | "$(UINODES)/ui_colour_picker", 16 | "$(UINODES)/ui_text_input" 17 | ] 18 | }, 19 | "preload": [ 20 | "ScrollerBehaviors", 21 | "ui_nodes", 22 | "ui_templates", 23 | "ui_colour_picker", 24 | "ui_text_input" 25 | ], 26 | "config": { 27 | "ui_text_input": true 28 | }, 29 | "resources":{ 30 | "*": [ 31 | "$(UIASSETS)/ui_colour_picker" 32 | ], 33 | "*-mask": [ 34 | "$(UIASSETS)/Roboto-Medium-12", 35 | "$(UIASSETS)/Roboto-Medium-18", 36 | "$(UIASSETS)/Roboto-Regular-18", 37 | "$(UIASSETS)/glyphs", 38 | "$(UIASSETS)/button", 39 | "$(UIASSETS)/popup", 40 | "$(UIASSETS)/slider", 41 | "$(UIASSETS)/switch", 42 | "$(UIASSETS)/ui_colour_picker_mask" 43 | ] 44 | }, 45 | "platforms": { 46 | "esp32": { 47 | "include": [ 48 | "$(MODDABLE)/modules/piu/MC/outline/manifest.json" 49 | ], 50 | "modules": { 51 | "*": [ 52 | "$(UINODES)/ui_chart", 53 | "$(UINODES)/ui_gauge" 54 | ] 55 | }, 56 | "preload": [ 57 | "ui_chart", 58 | "ui_gauge" 59 | ] 60 | }, 61 | "mac": { 62 | "include": [ 63 | "$(MODDABLE)/modules/piu/MC/outline/manifest.json" 64 | ], 65 | "modules": { 66 | "*": [ 67 | "$(UINODES)/ui_chart", 68 | "$(UINODES)/ui_gauge" 69 | ] 70 | }, 71 | "preload": [ 72 | "ui_chart", 73 | "ui_gauge" 74 | ] 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /mods/host/main.js: -------------------------------------------------------------------------------- 1 | import "nodered"; // import for global side effects 2 | import Modules from "modules"; 3 | 4 | if (!Modules.has("flows")) 5 | trace("no flows installed\n"); 6 | else { 7 | 8 | const flows = Modules.importNow("flows"); 9 | RED.build(flows); 10 | 11 | if (globalThis.REDTheme) { 12 | if (!Modules.has("ui_nodes") || !Modules.has("ui_templates")) 13 | trace("flow neeeds UI nodes; not build into host \n"); 14 | else { 15 | const buildModel = Modules.importNow("ui_nodes"); 16 | const templates = Modules.importNow("ui_templates"); 17 | const REDApplication = templates.REDApplication; 18 | if (REDApplication) { 19 | try { 20 | const model = buildModel(); 21 | new REDApplication(model, { commandListLength:4096, displayListLength:+4096, touchCount:1, pixels: 240 * 48 }); 22 | } 23 | catch { 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /mods/host/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "../../manifest_host.json" 4 | ], 5 | "modules": { 6 | "*": "./main" 7 | }, 8 | "strip": [ 9 | "Atomics", 10 | "eval", 11 | "FinalizationRegistry", 12 | "Function", 13 | "Generator", 14 | "WeakMap", 15 | "WeakRef", 16 | "WeakSet" 17 | ], 18 | "defines": { 19 | "XS_MODS": 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /mods/mod/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": "$(MODDABLE)/examples/manifest_mod.json", 3 | "modules": { 4 | "*": [ 5 | { 6 | "source": "../../flows", 7 | "transform": "nodered2mcu" 8 | } 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /mods/readme.md: -------------------------------------------------------------------------------- 1 | # Using Mods with Node-RED MCU Edition 2 | Copyright 2023, Moddable Tech, Inc. All rights reserved.
3 | Peter Hoddie
4 | Updated January 9, 2023
5 | 6 | ## Introduction 7 | Mods are a Moddable SDK feature for adding JavaScript code into a firmware image. For Node-RED MCU Edition mods allow you to install most of the code once and then install your Node-RED flows on top of that. Building and installing the mod is considerably faster, making for much faster development turnaround. Mods are compiled to byte-code on your development computer before being installed. 8 | 9 | To learn about Mods in-depth see the [Mods documentation](https://github.com/Moddable-OpenSource/moddable/blob/public/documentation/xs/mods.md). 10 | 11 | ## Using Mods 12 | 13 | ### Build & Install the Host 14 | Node-RED MCU Edition provides a basic mod host. It is built and installed using `mcconfig`: 15 | 16 | ``` 17 | cd $(PATH TO node-red-mcu) 18 | mcconfig -d -m -p esp32 ./mods/host/manifest.json 19 | ``` 20 | ### Build & Run a Mod 21 | Use `mcrun` to install the flows. If your flows are located at `(PATH to node-red-mcu)/flows.json`, do this: 22 | 23 | ``` 24 | cd $(PATH TO node-red-mcu) 25 | mcrun -d -m -p esp32 ./mods/mod/manifest.json 26 | ``` 27 | 28 | ### Configuring Wi-Fi 29 | There are two options for configuring Wi-Fi: 30 | 31 | 1. Pass the ssid and password to `mcconfig` as usual. These will be the default credentials for any mod loaded 32 | 2. Pass the ssid and password to `mcrun`. These will be used in place of the default credentials 33 | 34 | ``` 35 | mcrun -d -m -p esp32 ssid="my wi-fi" password="secret" ./mods/mod/manifest.json 36 | ``` 37 | 38 | ## Notes 39 | 40 | - It is important to use the correct platform identifier (`esp32`, `esp32/moddable_two`, etc) for your target device. If there is a mismatch between the platform used to build the host and to build the mod, the mod may not work. 41 | - Not all hosts support mods. If the installed host does not support mods, `mcrun` reports an error. 42 | - Installing mods with `mcrun` requires a debug build. Instrumented and Release builds do not support mods in Node-RED MCU Edition at this time. 43 | - Using a Host plus Mod requires more flash storage and memory. As a result, Mods are not practical for all projects on all devices. For example, they are not recommended on ESP8266. 44 | - Mods are supported in the `mcsim` simulator on macOS, Windows, and Linux. 45 | -------------------------------------------------------------------------------- /node_types.json: -------------------------------------------------------------------------------- 1 | { 2 | "catch": "", 3 | "change": "", 4 | "complete": "", 5 | "debug": "", 6 | "function": "", 7 | "group": "", 8 | "inject": "", 9 | "json": "", 10 | "link call": "", 11 | "link in": "", 12 | "link out": "", 13 | "range": "", 14 | "rbe": "", 15 | "status": "", 16 | "switch": "", 17 | "unknown": "", 18 | 19 | "lower-case": "$(NODEREDMCU)/examples/lower-case/manifest.json", 20 | 21 | "audioout": "$(NODEREDMCU)/nodes/audioout/manifest.json", 22 | 23 | "ui_base": "$(NODEREDMCU)/nodes/dashboard/manifest.json", 24 | "ui_button": "$(NODEREDMCU)/nodes/dashboard/manifest.json", 25 | "ui_chart": "$(NODEREDMCU)/nodes/dashboard/manifest.json", 26 | "ui_colour_picker": "$(NODEREDMCU)/nodes/dashboard/manifest.json", 27 | "ui_dropdown": "$(NODEREDMCU)/nodes/dashboard/manifest.json", 28 | "ui_gauge": "$(NODEREDMCU)/nodes/dashboard/manifest.json", 29 | "ui_group": "$(NODEREDMCU)/nodes/dashboard/manifest.json", 30 | "ui_numeric": "$(NODEREDMCU)/nodes/dashboard/manifest.json", 31 | "ui_slider": "$(NODEREDMCU)/nodes/dashboard/manifest.json", 32 | "ui_spacer": "$(NODEREDMCU)/nodes/dashboard/manifest.json", 33 | "ui_switch": "$(NODEREDMCU)/nodes/dashboard/manifest.json", 34 | "ui_tab": "$(NODEREDMCU)/nodes/dashboard/manifest.json", 35 | "ui_template": "$(NODEREDMCU)/nodes/dashboard/manifest.json", 36 | "ui_text_input": "$(NODEREDMCU)/nodes/dashboard/manifest.json", 37 | "ui_text": "$(NODEREDMCU)/nodes/dashboard/manifest.json", 38 | "ui_toast": "$(NODEREDMCU)/nodes/dashboard/manifest.json", 39 | 40 | "delay": "$(NODEREDMCU)/nodes/function/delay/manifest.json", 41 | "random": "$(NODEREDMCU)/nodes/function/random/manifest.json", 42 | "template": "$(NODEREDMCU)/nodes/function/template/manifest.json", 43 | "trigger": "$(NODEREDMCU)/nodes/function/trigger/manifest.json", 44 | 45 | "mcu_analog": "$(NODEREDMCU)/nodes/mcu/analog/manifest.json", 46 | "mcu_clock": "$(NODEREDMCU)/nodes/mcu/clock/manifest.json", 47 | "mcu_digital_in": "$(NODEREDMCU)/nodes/mcu/digital/manifest.json", 48 | "mcu_digital_out": "$(NODEREDMCU)/nodes/mcu/digital/manifest.json", 49 | "mcu_i2c_in": "$(NODEREDMCU)/nodes/mcu/i2c/manifest.json", 50 | "mcu_i2c_out": "$(NODEREDMCU)/nodes/mcu/i2c/manifest.json", 51 | "mcu_neopixels": "$(NODEREDMCU)/nodes/mcu/neopixels/manifest.json", 52 | "mcu_pulse_count": "$(NODEREDMCU)/nodes/mcu/pulsecount/manifest.json", 53 | "mcu_pulse_width": "$(NODEREDMCU)/nodes/mcu/pulsewidth/manifest.json", 54 | "mcu_pwm_out": "$(NODEREDMCU)/nodes/mcu/pwm/manifest.json", 55 | "sensor": "$(NODEREDMCU)/nodes/mcu/sensor/manifest.json", 56 | 57 | "http request": "$(NODEREDMCU)/nodes/network/httprequest/manifest.json", 58 | "http in": "$(NODEREDMCU)/nodes/network/httpserver/manifest.json", 59 | "http response": "$(NODEREDMCU)/nodes/network/httpserver/manifest.json", 60 | "mqtt-broker": "$(NODEREDMCU)/nodes/network/mqtt/manifest.json", 61 | "mqtt in": "$(NODEREDMCU)/nodes/network/mqtt/manifest.json", 62 | "mqtt out": "$(NODEREDMCU)/nodes/network/mqtt/manifest.json", 63 | "tcp in": "$(NODEREDMCU)/nodes/network/tcp/manifest.json", 64 | "tcp out": "$(NODEREDMCU)/nodes/network/tcp/manifest.json", 65 | "tls-config": "$(NODEREDMCU)/nodes/network/tls-config/manifest.json", 66 | "udp in": "$(NODEREDMCU)/nodes/network/udp/manifest.json", 67 | "udp out": "$(NODEREDMCU)/nodes/network/udp/manifest.json", 68 | "websocket-client": "$(NODEREDMCU)/nodes/network/websocketnodes/manifest.json", 69 | "websocket-listener": "$(NODEREDMCU)/nodes/network/websocketnodes/manifest.json", 70 | "websocket in": "$(NODEREDMCU)/nodes/network/websocketnodes/manifest.json", 71 | "websocket out": "$(NODEREDMCU)/nodes/network/websocketnodes/manifest.json", 72 | 73 | "csv": "$(NODEREDMCU)/nodes/parser/csv/manifest.json", 74 | 75 | "rpi-ds18b20": "$(NODEREDMCU)/nodes/rpi/rpi-ds18b20/manifest.json", 76 | "rpi-gpio in": "$(NODEREDMCU)/nodes/rpi/rpi-gpio/manifest.json", 77 | "rpi-gpio out": "$(NODEREDMCU)/nodes/rpi/rpi-gpio/manifest.json", 78 | "rpi-neopixels": "$(NODEREDMCU)/nodes/rpi/rpi-neopixels/manifest.json", 79 | 80 | "batch": "$(NODEREDMCU)/nodes/sequence/batch/manifest.json", 81 | "join": "$(NODEREDMCU)/nodes/sequence/join/manifest.json", 82 | "sort": "$(NODEREDMCU)/nodes/sequence/sort/manifest.json", 83 | "split": "$(NODEREDMCU)/nodes/sequence/split/manifest.json", 84 | 85 | "file": "$(NODEREDMCU)/nodes/storage/file/manifest.json", 86 | "file in": "$(NODEREDMCU)/nodes/storage/file/manifest.json", 87 | 88 | "openweathermap": "$(NODEREDMCU)/nodes/weather/openweathermap/manifest.json", 89 | "openweathermap in": "$(NODEREDMCU)/nodes/weather/openweathermap/manifest.json" 90 | } 91 | -------------------------------------------------------------------------------- /nodered.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022-2023 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | #include "xsmc.h" 22 | 23 | #include "xsHost.h" 24 | 25 | // 16 byte string. random hex values. 26 | void xs_nodered_util_generateId(xsMachine *the) 27 | { 28 | char id[16]; 29 | size_t i; 30 | static const char hex[] = "0123456789abcdef"; 31 | 32 | for (i = 0; i < sizeof(id); i+= 4) { 33 | int r = c_rand(); 34 | id[i + 0] = hex[r & 0x0f]; 35 | id[i + 1] = hex[(r >> 4) & 0x0f]; 36 | id[i + 2] = hex[(r >> 8) & 0x0f]; 37 | id[i + 3] = hex[(r >> 12) & 0x0f]; 38 | } 39 | 40 | xsmcSetStringBuffer(xsResult, id, sizeof(id)); 41 | } 42 | 43 | void xs_nodered_util_debugging(xsMachine *the) 44 | { 45 | #ifdef mxDebug 46 | extern xsBooleanValue fxIsConnected(xsMachine* the); 47 | xsmcSetBoolean(xsResult, fxIsConnected(the)); 48 | #endif 49 | } 50 | 51 | void xs_buffer_prototype_indexOf(xsMachine *the) 52 | { 53 | int offset = 0; 54 | uint8_t *needle, *haystack, *initialHaystack; 55 | xsUnsignedValue needleLength, haystackLength; 56 | 57 | if (xsmcArgc > 1) { 58 | offset = xsmcToInteger(xsArg(1)); 59 | if (xsmcArgc > 2) 60 | xsUnknownError("unsupported"); 61 | } 62 | 63 | if (xsReferenceType != xsmcTypeOf(xsArg(0))) 64 | xsUnknownError("unsupported"); // valid to pass string or number for arg(0) 65 | 66 | xsmcGetBufferReadable(xsArg(0), (void **)&needle, &needleLength); 67 | xsmcGetBufferReadable(xsThis, (void **)&haystack, &haystackLength); 68 | initialHaystack = haystack; 69 | 70 | if (0 == offset) 71 | ; 72 | else if ((offset < 0) && (-offset < (int)haystackLength)) { 73 | haystack += haystackLength + offset; 74 | haystackLength = -offset; 75 | } 76 | else if ((offset > 0) && (offset < (int)haystackLength)) { 77 | haystack += offset; 78 | haystackLength -= offset; 79 | } 80 | else 81 | xsUnknownError("invalid offset"); 82 | 83 | if (needleLength <= haystackLength) { 84 | uint8_t *lastHaystack = haystack + haystackLength - needleLength; 85 | for ( ; haystack <= lastHaystack; haystack++) { 86 | if (0 == c_memcmp(haystack, needle, needleLength)) { 87 | xsmcSetInteger(xsResult, haystack - initialHaystack); 88 | return; 89 | } 90 | } 91 | } 92 | 93 | xsmcSetInteger(xsResult, -1); 94 | } 95 | 96 | void xs_buffer_prototype_lastIndexOf(xsMachine *the) 97 | { 98 | int offset; 99 | uint8_t *needle, *haystack, *initialHaystack; 100 | xsUnsignedValue needleLength, haystackLength; 101 | 102 | if (xsmcArgc > 1) { 103 | offset = xsmcToInteger(xsArg(1)); 104 | if (xsmcArgc > 2) 105 | xsUnknownError("unsupported"); 106 | } 107 | 108 | if (xsReferenceType != xsmcTypeOf(xsArg(0))) 109 | xsUnknownError("unsupported"); // valid to pass string or number for arg(0) 110 | 111 | xsmcGetBufferReadable(xsArg(0), (void **)&needle, &needleLength); 112 | xsmcGetBufferReadable(xsThis, (void **)&haystack, &haystackLength); 113 | initialHaystack = haystack; 114 | 115 | if (xsmcArgc < 2) 116 | haystack = initialHaystack + haystackLength - 1; 117 | else if (0 == offset) 118 | ; 119 | else if ((offset < 0) && (-offset < (int)haystackLength)) { 120 | haystack += haystackLength + offset; 121 | } 122 | else if ((offset > 0) && (offset < (int)haystackLength)) { 123 | haystack += offset; 124 | } 125 | else 126 | xsUnknownError("invalid offset"); 127 | 128 | if (needleLength <= haystackLength) { 129 | if ((haystack + needleLength) > (initialHaystack + haystackLength)) 130 | haystack = initialHaystack + haystackLength - needleLength; 131 | for ( ; haystack >= initialHaystack; haystack--) { 132 | if (0 == c_memcmp(haystack, needle, needleLength)) { 133 | xsmcSetInteger(xsResult, haystack - initialHaystack); 134 | return; 135 | } 136 | } 137 | } 138 | 139 | xsmcSetInteger(xsResult, -1); 140 | } 141 | 142 | -------------------------------------------------------------------------------- /nodes/audioout/audioout.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | import {Node} from "nodered"; 22 | 23 | import Resource from "Resource" 24 | import WavStreamer from "wavstreamer"; 25 | import SBCStreamer from "sbcstreamer"; 26 | import URL from "url"; 27 | import Modules from "modules"; 28 | 29 | let audioOut; 30 | 31 | class AudioOutNode extends Node { 32 | #stream; 33 | #streamer; 34 | 35 | onStart(config) { 36 | super.onStart(config); 37 | 38 | if (!audioOut) { 39 | const AudioOut = Modules.importNow("pins/audioout") 40 | audioOut = new AudioOut({}); 41 | audioOut.callbacks = []; 42 | audioOut.start(); 43 | audioOut.stream = 0; 44 | } 45 | else if (audioOut.stream === audioOut.streams) 46 | return void this.error("no more audio streams available") 47 | 48 | this.#stream = audioOut.stream++; 49 | audioOut.enqueue(this.#stream, audioOut.constructor.Volume, 256 * Number(config.volume ?? 1)); 50 | } 51 | onMessage(msg, done) { 52 | const AudioOut = audioOut.constructor; 53 | 54 | if (msg.volume) 55 | audioOut.enqueue(this.#stream, AudioOut.Volume, 256 * Number(msg.volume)); 56 | 57 | const play = msg.wave || msg.tones || msg.sbc || msg.resource || msg.samples; 58 | if (play || msg.flush) { 59 | audioOut.enqueue(this.#stream, AudioOut.Flush); 60 | this.#streamer?.close?.(); 61 | this.#streamer?.done?.(); 62 | this.#streamer = undefined; 63 | audioOut.callbacks[this.#stream] = undefined; 64 | } 65 | if (!play) 66 | return void done(); 67 | 68 | if (msg.wave || msg.sbc) { 69 | let url, Streamer; 70 | if (msg.wave) { 71 | url = new URL(msg.wave); 72 | Streamer = WavStreamer; 73 | } 74 | else { 75 | url = new URL(msg.sbc); 76 | Streamer = SBCStreamer; 77 | } 78 | 79 | this.#streamer = new Streamer({ 80 | http: device.network.http, 81 | port: url.port || 80, 82 | host: url.hostname, 83 | path: url.pathname + url.search, 84 | waveHeaderBytes: 2048, // WavStreamer only 85 | audio: { 86 | out: audioOut, 87 | stream: this.#stream 88 | }, 89 | onError: e => { 90 | this.#streamer.done(e); 91 | delete this.#streamer.done; 92 | }, 93 | onDone: () => { 94 | this.#streamer.done() 95 | delete this.#streamer.done; 96 | } 97 | }); 98 | this.#streamer.done = done; 99 | } 100 | else if (msg.resource) { 101 | let resource; 102 | if (Resource.exists(msg.resource)) 103 | resource = new Resource(msg.resource); 104 | else if (Resource.exists(msg.resource + ".maud")) 105 | resource = new Resource(msg.resource + ".maud"); 106 | else 107 | return void done(`resource "${msg.resource}" not found`); 108 | audioOut.enqueue(this.#stream, AudioOut.Samples, resource); 109 | audioOut.enqueue(this.#stream, AudioOut.Callback, 0); 110 | audioOut.callbacks[this.#stream] = () => { 111 | this.#streamer.done(); 112 | this.#streamer = undefined; 113 | }; 114 | this.#streamer = {done}; 115 | } 116 | else if (msg.tones) { 117 | this.#streamer = { 118 | tones: msg.tones, 119 | position: 0, 120 | done 121 | }; 122 | audioOut.callbacks[this.#stream] = last => { 123 | const streamer = this.#streamer, tones = streamer.tones, length = tones.length; 124 | if (last) { 125 | streamer.done(); 126 | this.#streamer = undefined; 127 | return; 128 | } 129 | let remaining = audioOut.length(this.#stream), scale = audioOut.sampleRate / 1000; 130 | let position = streamer.position; 131 | while ((position < length) && (--remaining > 0)) { 132 | const [frequency, duration] = tones[position++]; 133 | audioOut.enqueue(this.#stream, AudioOut.Tone, frequency, duration * scale); 134 | } 135 | streamer.position = position; 136 | audioOut.enqueue(this.#stream, AudioOut.Callback, position === length); 137 | }; 138 | audioOut.callbacks[this.#stream](0); 139 | } 140 | else if (msg.samples) { 141 | let samples = msg.samples; 142 | if (samples.buffer instanceof ArrayBuffer) { 143 | samples = new Uint8Array(new SharedArrayBuffer(samples.length)) 144 | samples.set(msg.samples); 145 | } 146 | audioOut.enqueue(this.#stream, AudioOut.RawSamples, samples); 147 | audioOut.enqueue(this.#stream, AudioOut.Callback, 0); 148 | audioOut.callbacks[this.#stream] = () => { 149 | this.#streamer.done(); 150 | this.#streamer = undefined; 151 | }; 152 | this.#streamer = {done, samples}; 153 | } 154 | else 155 | done(); 156 | } 157 | 158 | static type = "audioout"; 159 | static { 160 | RED.nodes.registerType(this.type, this); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /nodes/audioout/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/io/tcp/httpclient/manifest_httpclient.json", 4 | "$(MODDABLE)/modules/data/wavreader/manifest.json", 5 | "$(MODDABLE)/modules/data/url/manifest.json" 6 | ], 7 | "modules": { 8 | "audioout": "./audioout", 9 | "pins/*": [ 10 | "$(MODULES)/pins/i2s/*" 11 | ], 12 | "*": [ 13 | "$(MODDABLE)/examples/pins/audioout/http-stream/wavstreamer", 14 | "$(MODDABLE)/examples/pins/audioout/http-stream/sbcstreamer" 15 | ] 16 | }, 17 | "preload": [ 18 | "audioout", 19 | "pins/audioout", 20 | "wavstreamer", 21 | "sbcstreamer" 22 | ], 23 | "defines": { 24 | "audioOut": { 25 | "bitsPerSample": 16, 26 | "numChannels": 1, 27 | "sampleRate": 16000, 28 | "queueLength": 24 29 | } 30 | }, 31 | "config": { 32 | "startupSound": false 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /nodes/audioout/mcu_audioout.html: -------------------------------------------------------------------------------- 1 | 2 | 26 | 27 | 28 | 85 | 86 | 97 | 98 | 123 | -------------------------------------------------------------------------------- /nodes/audioout/mcu_audioout.js: -------------------------------------------------------------------------------- 1 | module.exports = function(RED) { 2 | function AudioOutNode(config) { 3 | RED.nodes.createNode(this,config); 4 | 5 | this.on('input', (msg, send, done) => { 6 | done(); 7 | }); 8 | } 9 | RED.nodes.registerType("audioout", AudioOutNode); 10 | } 11 | -------------------------------------------------------------------------------- /nodes/audioout/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@moddable-node-red/audioout", 3 | "version": "1.0.0", 4 | "description": "example", 5 | "license": "LGPLv3", 6 | "node-red" : { 7 | "nodes": { 8 | "mcu_audioout": "mcu_audioout.js" 9 | } 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /nodes/dashboard/assets/Roboto-Medium-12.fnt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/nodes/dashboard/assets/Roboto-Medium-12.fnt -------------------------------------------------------------------------------- /nodes/dashboard/assets/Roboto-Medium-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/nodes/dashboard/assets/Roboto-Medium-12.png -------------------------------------------------------------------------------- /nodes/dashboard/assets/Roboto-Medium-18.fnt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/nodes/dashboard/assets/Roboto-Medium-18.fnt -------------------------------------------------------------------------------- /nodes/dashboard/assets/Roboto-Medium-18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/nodes/dashboard/assets/Roboto-Medium-18.png -------------------------------------------------------------------------------- /nodes/dashboard/assets/Roboto-Regular-18.fnt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/nodes/dashboard/assets/Roboto-Regular-18.fnt -------------------------------------------------------------------------------- /nodes/dashboard/assets/Roboto-Regular-18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/nodes/dashboard/assets/Roboto-Regular-18.png -------------------------------------------------------------------------------- /nodes/dashboard/assets/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/nodes/dashboard/assets/button.png -------------------------------------------------------------------------------- /nodes/dashboard/assets/glyphs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/nodes/dashboard/assets/glyphs.png -------------------------------------------------------------------------------- /nodes/dashboard/assets/popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/nodes/dashboard/assets/popup.png -------------------------------------------------------------------------------- /nodes/dashboard/assets/slider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/nodes/dashboard/assets/slider.png -------------------------------------------------------------------------------- /nodes/dashboard/assets/switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/nodes/dashboard/assets/switch.png -------------------------------------------------------------------------------- /nodes/dashboard/assets/ui_colour_picker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/nodes/dashboard/assets/ui_colour_picker.png -------------------------------------------------------------------------------- /nodes/dashboard/assets/ui_colour_picker_mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/nodes/dashboard/assets/ui_colour_picker_mask.png -------------------------------------------------------------------------------- /nodes/dashboard/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_base.json", 4 | "$(MODDABLE)/examples/manifest_piu.json", 5 | "$(MODDABLE)/modules/input/expanding-keyboard/vertical/manifest.json", 6 | "$(MODDABLE)/modules/base/deepEqual/manifest.json", 7 | "$(MODDABLE)/modules/base/structuredClone/manifest.json", 8 | "$(MODDABLE)/modules/base/modules/manifest.json", 9 | "$(MODDABLE)/modules/data/base64/manifest.json", 10 | "$(MODDABLE)/modules/data/hex/manifest.json" 11 | ], 12 | "modules": { 13 | "*": [ 14 | "../../nodered", 15 | "../function/template/mustache", 16 | "./ScrollerBehaviors", 17 | "./ui_nodes", 18 | "./ui_templates", 19 | "./ui_colour_picker", 20 | "./ui_text_input" 21 | ] 22 | }, 23 | "preload": [ 24 | "nodered", 25 | "mustache", 26 | "ScrollerBehaviors", 27 | "ui_nodes", 28 | "ui_templates", 29 | "ui_colour_picker", 30 | "ui_text_input" 31 | ], 32 | "resources":{ 33 | "*": [ 34 | "./assets/ui_colour_picker" 35 | ], 36 | "*-mask": [ 37 | "./assets/Roboto-Medium-12", 38 | "./assets/Roboto-Medium-18", 39 | "./assets/Roboto-Regular-18", 40 | "./assets/glyphs", 41 | "./assets/button", 42 | "./assets/popup", 43 | "./assets/slider", 44 | "./assets/switch", 45 | "./assets/ui_colour_picker_mask" 46 | ] 47 | }, 48 | "platforms": { 49 | "esp32": { 50 | "include": [ 51 | "$(MODDABLE)/modules/piu/MC/outline/manifest.json" 52 | ], 53 | "modules": { 54 | "*": [ 55 | "./ui_chart", 56 | "./ui_gauge" 57 | ] 58 | }, 59 | "preload": [ 60 | "ui_chart", 61 | "ui_gauge" 62 | ] 63 | }, 64 | "mac": { 65 | "include": [ 66 | "$(MODDABLE)/modules/piu/MC/outline/manifest.json" 67 | ], 68 | "modules": { 69 | "*": [ 70 | "./ui_chart", 71 | "./ui_gauge" 72 | ] 73 | }, 74 | "preload": [ 75 | "ui_chart", 76 | "ui_gauge" 77 | ] 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /nodes/dashboard/ui_text_input.js: -------------------------------------------------------------------------------- 1 | import { UIControlNode, registerConstructor } from "ui_nodes"; 2 | 3 | class UITextInputNode extends UIControlNode { 4 | constructor(id, flow, name) { 5 | super(id, flow, name); 6 | } 7 | onChanged() { 8 | this.msg.payload = this.value; 9 | this.msg.topic = this.topic({}); 10 | this.send(this.msg); 11 | } 12 | onMessage(msg) { 13 | this.value = msg.payload; 14 | this.container?.delegate("onUpdate"); 15 | if (this.passthru && (this.msg._msgid != msg._msgid)) 16 | this.send(msg); 17 | } 18 | onStart(config) { 19 | super.onStart(config); 20 | this.label = config.label; 21 | this.mode = config.mode; 22 | this.passthru = config.passthru; 23 | this.topic = config.topic; 24 | this.value = ""; 25 | 26 | this.Template = this.lookupTemplate(config, REDTextInput); 27 | 28 | this.msg = { }; 29 | } 30 | } 31 | registerConstructor("ui_text_input", UITextInputNode); 32 | 33 | import {} from "piu/MC"; 34 | import {VerticalExpandingKeyboard} from "keyboard"; 35 | import {KeyboardField} from "common/keyboard"; 36 | 37 | import { 38 | ButtonBehavior, 39 | REDBehavior, 40 | UNIT 41 | } from "./ui_templates"; 42 | 43 | const REDKeyboard = Container.template($ => ({ 44 | left:0, right:0, top:0, bottom:0, active:true, 45 | Behavior: class extends Behavior { 46 | onCreate(container, data) { 47 | this.data = data; 48 | data.KEYBOARD.add(new VerticalExpandingKeyboard(data, { 49 | style:REDTheme.styles.keyboard, target:data.FIELD, doTransition:false 50 | })); 51 | } 52 | onKeyboardOK(container, string) { 53 | this.data.container.defer("onTextInputOK", string); 54 | } 55 | onTouchEnded(layout, id, x, y, ticks) { 56 | this.data.container.defer("onTextInputCancel"); 57 | } 58 | }, 59 | contents:[ 60 | Container($, { 61 | anchor:"KEYBOARD", left:0, right:0, height:185, bottom:0, 62 | }), 63 | ] 64 | })); 65 | 66 | const REDTextField = Container.template($ => ({ 67 | anchor:"FIELD", left:0, right:0, top:0, bottom:0, active:true, 68 | Behavior: class extends ButtonBehavior { 69 | onTap(container) { 70 | container.bubble("onPrompt"); 71 | } 72 | onDisplaying(container) { 73 | super.onDisplaying(container); 74 | this.onUpdate(container); 75 | } 76 | onUpdate(container) { 77 | const data = this.data; 78 | let string = data.value; 79 | if (data.mode == "password") 80 | string = "*".repeat(string.length); 81 | container.last.string = string; 82 | } 83 | }, 84 | contents: [ 85 | Content($, { left:10, right:10, height:1, bottom:3, skin:REDTheme.skins.textField }), 86 | Label($, { left:0, right:0, top:0, bottom:0, style:REDTheme.styles.textField }), 87 | ], 88 | })); 89 | 90 | class REDKeyboardInputBehavior extends REDBehavior { 91 | getMovablePart(container) { 92 | debugger; 93 | } 94 | onPrompt(container) { 95 | const data = this.data; 96 | const former = data.FIELD; 97 | const current = new KeyboardField(data, { anchor:"FIELD", password:data.mode == "password", left:0, right:0, top:0, bottom:0, style:REDTheme.styles.textField, string:data.value }); 98 | current.first.state = current.last.state = 1; 99 | former.container.replace(former, current); 100 | application.add(new REDKeyboard(data)); 101 | const part = this.getMovablePart(container); 102 | this.y = part.y; 103 | part.y = (this.y + screen.height - 177 - UNIT) - current.y; 104 | } 105 | onTextInputCancel(container) { 106 | const application = container.application; 107 | if (application) { 108 | const data = this.data; 109 | const former = data.FIELD; 110 | const current = new REDTextField(data, {}); 111 | former.container.replace(former, current); 112 | delete data.KEYBOARD; 113 | const part = this.getMovablePart(container); 114 | part.y = this.y; 115 | application.remove(application.last); 116 | } 117 | } 118 | onTextInputOK(container, value) { 119 | const data = this.data; 120 | data.value = value; 121 | this.onTextInputCancel(container); 122 | } 123 | onUndisplaying(container) { 124 | super.onUndisplaying(container); 125 | delete this.data.FIELD; 126 | } 127 | } 128 | 129 | class REDTextInputBehavior extends REDKeyboardInputBehavior { 130 | getMovablePart(container) { 131 | return application.first.first; 132 | } 133 | onTextInputOK(container, value) { 134 | super.onTextInputOK(container, value); 135 | this.data.onChanged(); 136 | } 137 | } 138 | const REDTextInput = Row.template($ => ({ 139 | left:$.left, width:$.width, top:$.top, height:$.height, clip:true, Behavior:REDTextInputBehavior, 140 | contents: [ 141 | $.label ? Label($, { height:UNIT, style:REDTheme.styles.textNameLeft, string:$.label }) : null, 142 | Container($, { 143 | left:0, right:0, top:0, bottom:0, 144 | contents: [ 145 | REDTextField($, {}), 146 | ] 147 | }), 148 | ], 149 | })); 150 | 151 | class REDToastPromptBehavior extends REDKeyboardInputBehavior { 152 | getMovablePart(container) { 153 | return container.first; 154 | } 155 | onClose(container, value) { 156 | const application = container.application; 157 | if (application) 158 | application.remove(container); 159 | this.data.value = value; 160 | this.data.onChanged(); 161 | } 162 | onDisplaying(container) { 163 | super.onDisplaying(container); 164 | const column = container.first; 165 | column.coordinates = { width:column.width, top:column.y - container.y, height:column.height }; 166 | } 167 | } 168 | const REDToastPrompt = Container.template($ => ({ 169 | left:0, right:0, top:0, bottom:0, skin:REDTheme.skins.menuBackground, Behavior:REDToastPromptBehavior, 170 | contents: [ 171 | Column($, { 172 | width:240, skin:REDTheme.skins.toast, 173 | contents: [ 174 | Text($, { left:0, right:0, style:REDTheme.styles.notification, string:$.text }), 175 | Container($, { 176 | left:0, right:0, height:UNIT, 177 | contents: [ 178 | REDTextField($, {}), 179 | ], 180 | }), 181 | Content($, { height: 10 }), 182 | Row($, { 183 | right:10, height:UNIT, 184 | contents: [ 185 | ($.cancel) ? Container($, { 186 | width:110, height:UNIT, skin:REDTheme.skins.button, clip:true, active:true, 187 | Behavior: class extends ButtonBehavior{ 188 | onTap(container) { 189 | container.bubble("onClose", this.data.cancel); 190 | } 191 | }, 192 | contents: [ 193 | Label($, { top:0, bottom:0, style:REDTheme.styles.button, string:$.cancel }), 194 | ], 195 | }) : null, 196 | Container($, { 197 | width:110, height:UNIT, skin:REDTheme.skins.button, clip:true, active:true, 198 | Behavior: class extends ButtonBehavior{ 199 | onTap(container) { 200 | container.bubble("onClose", this.data.value ?? this.data.ok); 201 | } 202 | }, 203 | contents: [ 204 | Label($, { top:0, bottom:0, style:REDTheme.styles.button, string:$.ok }), 205 | ], 206 | }), 207 | ], 208 | }), 209 | Content($, { height: 10 }), 210 | ] 211 | }), 212 | ], 213 | })); 214 | export default REDToastPrompt; 215 | -------------------------------------------------------------------------------- /nodes/function/delay/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "delay": "./89-delay" 4 | }, 5 | "preload": "delay" 6 | } 7 | -------------------------------------------------------------------------------- /nodes/function/random/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "random": "./random" 4 | }, 5 | "preload": [ 6 | "random" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /nodes/function/random/random.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | import {Node} from "nodered"; 22 | 23 | class RandomNode extends Node { 24 | onStart(config) { 25 | super.onStart(config); 26 | 27 | Object.defineProperty(this, "onMessage", {value: config.onMessage}); 28 | } 29 | 30 | static type = "random"; 31 | static { 32 | RED.nodes.registerType(this.type, this); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /nodes/function/template/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "*": [ 4 | "./template", 5 | "./mustache" 6 | ] 7 | }, 8 | "preload": [ 9 | "template", 10 | "mustache" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /nodes/function/template/template.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | import {Node} from "nodered"; 22 | import Mustache from "mustache"; 23 | 24 | class Context extends Mustache.Context { 25 | #node; 26 | constructor(msg, node) { 27 | super(msg); 28 | this.#node = node; 29 | } 30 | lookup(name) { 31 | const result = super.lookup(name); 32 | if (result) 33 | return result; 34 | 35 | if (name.startsWith("env.")) 36 | return this.#node.getSetting(name.slice(4)); 37 | if (name.startsWith("flow.")) 38 | return this.#node.flow.get(name.slice(5)); 39 | if (name.startsWith("global.")) 40 | return globalContext.get(name.slice(7)); 41 | } 42 | } 43 | 44 | class Template extends Node { 45 | #template; 46 | #syntax; 47 | #output; 48 | 49 | onStart(config) { 50 | super.onStart(config); 51 | 52 | Mustache.templateCache = undefined; 53 | this.#template = config.template; 54 | this.#syntax = config.syntax; 55 | this.#output = config.output; 56 | } 57 | onMessage(msg, done) { 58 | const template = this.#template ?? msg.template; 59 | let result = template; 60 | if ("mustache" === this.#syntax) 61 | result = Mustache.render(template, new Context(msg, this)); 62 | if ("json" === this.#output) { 63 | try { 64 | result = JSON.parse(result); 65 | } 66 | catch (e) { 67 | this.error(e); 68 | return; 69 | } 70 | } 71 | msg.payload = result; 72 | done(); 73 | return msg; 74 | } 75 | 76 | static type = "template"; 77 | static { 78 | RED.nodes.registerType(this.type, this); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /nodes/function/trigger/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "trigger": "./89-trigger", 4 | "*": [ 5 | "../template/mustache" 6 | ] 7 | }, 8 | "preload": [ 9 | "trigger", 10 | "mustache" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /nodes/mcu/analog/analog.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022-2023 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | import {Node} from "nodered"; 22 | 23 | let cache; // support multiple nodes sharing the same pin, like the RPi implementation 24 | 25 | class AnalogIn extends Node { 26 | #io; 27 | 28 | onStart(config) { 29 | super.onStart(config); 30 | 31 | const Analog = globalThis.device?.io?.Analog; 32 | if (Analog) { 33 | cache ??= new Map; 34 | let io = cache.get(config.pin); 35 | if (!io) { 36 | const options = { 37 | pin: config.pin 38 | }; 39 | if (config.resolution) 40 | options.resolution = config.resolution; 41 | try { 42 | this.#io = io = new Analog(options); 43 | cache.set(config.pin, io); 44 | } 45 | catch { 46 | } 47 | } 48 | } 49 | 50 | if (!this.#io) 51 | this.status({fill: "red", shape: "dot", text: "node-red:common.status.error"}); 52 | } 53 | onMessage(msg) { 54 | const io = this.#io; 55 | if (!io) 56 | return; 57 | 58 | msg.resolution = io.resolution; 59 | msg.payload = io.read() / ((1 << msg.resolution) - 1); 60 | this.status({fill: "green", shape: "dot", text: msg.payload.toFixed(3)}); 61 | return msg; 62 | } 63 | 64 | static type = "mcu_analog"; 65 | static { 66 | RED.nodes.registerType(this.type, this); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /nodes/mcu/analog/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "*": [ 4 | "./analog" 5 | ] 6 | }, 7 | "preload": [ 8 | "analog" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /nodes/mcu/analog/mcu_analog.html: -------------------------------------------------------------------------------- 1 | 28 | 29 | 47 | 48 | 49 | 50 | 62 | -------------------------------------------------------------------------------- /nodes/mcu/analog/mcu_analog.js: -------------------------------------------------------------------------------- 1 | module.exports = function(RED) { 2 | function AnalogNode(config) { 3 | RED.nodes.createNode(this, config); 4 | console.log(config) 5 | } 6 | RED.nodes.registerType("mcu_analog", AnalogNode); 7 | } 8 | -------------------------------------------------------------------------------- /nodes/mcu/assets/ediit=sensor-node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/nodes/mcu/assets/ediit=sensor-node.png -------------------------------------------------------------------------------- /nodes/mcu/assets/edit-sensor-node-host-provider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/nodes/mcu/assets/edit-sensor-node-host-provider.png -------------------------------------------------------------------------------- /nodes/mcu/assets/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/nodes/mcu/assets/flow.png -------------------------------------------------------------------------------- /nodes/mcu/assets/palette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/nodes/mcu/assets/palette.png -------------------------------------------------------------------------------- /nodes/mcu/clock/clock.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022-2023 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | import {Node} from "nodered"; 22 | 23 | class Clock extends Node { 24 | #clock; 25 | 26 | onStart(config) { 27 | super.onStart(config); 28 | 29 | try { 30 | this.#clock = config.initialize.call(this); 31 | this.status({fill: "green", shape: "dot", text: "node-red:common.status.connected"}); 32 | } 33 | catch { 34 | this.status({fill: "red", shape: "ring", text: "node-red:common.status.disconnected"}); 35 | } 36 | } 37 | onMessage(msg, done) { 38 | if (!this.#clock) 39 | return; 40 | 41 | try { 42 | if (msg.configuration) { 43 | this.#clock.configure(msg.configuration); 44 | msg = undefined; 45 | } 46 | else if (msg.payload) { 47 | this.#clock.time = Number(msg.payload); 48 | msg = undefined; 49 | } 50 | else 51 | msg.payload = this.#clock.time; 52 | done(); 53 | } 54 | catch (e) { 55 | done(e); 56 | msg = undefined; 57 | } 58 | 59 | return msg; 60 | } 61 | 62 | static type = "mcu_clock"; 63 | static { 64 | RED.nodes.registerType(this.type, this); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /nodes/mcu/clock/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "mcu_clock": "./clock" 4 | }, 5 | "preload": "mcu_clock" 6 | } 7 | -------------------------------------------------------------------------------- /nodes/mcu/clock/mcu_clock.html: -------------------------------------------------------------------------------- 1 | 100 | 101 | 131 | 132 | 133 | 134 | 151 | -------------------------------------------------------------------------------- /nodes/mcu/clock/mcu_clock.js: -------------------------------------------------------------------------------- 1 | module.exports = function(RED) { 2 | function ClockNode(config) { 3 | RED.nodes.createNode(this,config); 4 | console.log(config) 5 | } 6 | RED.nodes.registerType("mcu_clock", ClockNode); 7 | } 8 | -------------------------------------------------------------------------------- /nodes/mcu/digital/digital.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022-2023 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | import {Node} from "nodered"; 22 | import Timer from "timer"; 23 | 24 | let cache; // support multiple nodes sharing the same pin, like the RPi implementation 25 | 26 | class DigitalInNode extends Node { 27 | #timer; 28 | 29 | onStart(config) { 30 | super.onStart(config); 31 | 32 | const Digital = globalThis.device?.io?.Digital; 33 | if (!Digital) 34 | return void this.status({fill: "red", shape: "dot", text: "node-red:common.status.error"}); 35 | 36 | if (config.debounce) 37 | Object.defineProperty(this, "debouce", {value: config.debounce}); 38 | 39 | let edge = config.edge; 40 | if (config.invert) { 41 | Object.defineProperty(this, "invert", {value: 1}); 42 | edge = ((edge & 1) << 1) | ((edge & 2) >> 1); 43 | } 44 | 45 | cache ??= new Map; 46 | let io = cache.get(config.pin); 47 | if (io) { 48 | if ((io.mode !== config.mode) || (io.edge !== edge)) 49 | return void this.status({fill: "red", shape: "dot", text: "mismatch"}); 50 | io.readers.push(this); 51 | } 52 | else { 53 | io = new Digital({ 54 | pin: config.pin, 55 | mode: Digital[config.mode], 56 | edge: ((edge & 1) ? Digital.Rising : 0) + ((edge & 2) ? Digital.Falling : 0), 57 | onReadable() { 58 | this.readers.forEach(reader => { 59 | reader.#timer ??= Timer.set(() => { 60 | reader.#timer = undefined; 61 | 62 | const msg = { 63 | payload: this.read() ^ (reader.invert ?? 0), 64 | topic: "gpio/" + this.pin 65 | }; 66 | reader.send(msg) 67 | reader.status({fill: "green", shape: "dot", text: msg.payload.toString()}); 68 | }, reader.debounce ?? 0); 69 | }); 70 | } 71 | }); 72 | io.mode = config.mode; 73 | io.edge = edge; 74 | io.pin = config.pin; 75 | io.readers = [this]; 76 | cache.set(config.pin, io); 77 | } 78 | 79 | if (config.initial) { 80 | const payload = io.read() ^ (this.invert ?? 0); 81 | this.send({ 82 | payload, 83 | topic: "gpio/" + config.pin 84 | }); 85 | this.status({fill: "green", shape: "dot", text: payload.toString()}); 86 | } 87 | } 88 | 89 | static type = "mcu_digital_in"; 90 | static { 91 | RED.nodes.registerType(this.type, this); 92 | } 93 | } 94 | 95 | class DigitalOutNode extends Node { 96 | #io; 97 | 98 | onStart(config) { 99 | super.onStart(config); 100 | 101 | if (!globalThis.device?.io?.Digital) 102 | return; 103 | 104 | if (config.invert) 105 | Object.defineProperty(this, "invert", {value: 1}); 106 | 107 | cache ??= new Map; 108 | let io = cache.get(config.pin); 109 | 110 | if (io) { 111 | if (io.mode !== config.mode) 112 | return void this.status({fill: "red", shape: "dot", text: "mismatch"}); 113 | this.#io = io; 114 | } 115 | else { 116 | try { 117 | this.#io = io = new device.io.Digital({ 118 | pin: config.pin, 119 | mode: device.io.Digital[config.mode] 120 | }); 121 | 122 | if (undefined !== config.initial) { 123 | if (0 == config.initial) 124 | io.write(0 ^ (this.invert ?? 0)); 125 | else if (1 == config.initial) 126 | io.write(1 ^ (this.invert ?? 0)); 127 | } 128 | io.mode = config.mode; 129 | cache.set(config.pin, io); 130 | } 131 | catch { 132 | this.status({fill: "red", shape: "dot", text: "node-red:common.status.error"}); 133 | } 134 | } 135 | } 136 | onMessage(msg, done) { 137 | if (this.#io) { 138 | const value = msg.payload ^ (this.invert ?? 0); 139 | this.#io.write(value); 140 | this.status({fill:"green", shape:"dot", text: value.toString()}); 141 | } 142 | done(); 143 | } 144 | 145 | static type = "mcu_digital_out"; 146 | static { 147 | RED.nodes.registerType(this.type, this); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /nodes/mcu/digital/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "*": [ 4 | "./digital" 5 | ] 6 | }, 7 | "preload": [ 8 | "digital" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /nodes/mcu/digital/mcu_digital.html: -------------------------------------------------------------------------------- 1 | 59 | 60 | 97 | 98 | 125 | 126 | 127 | 128 | 144 | 145 | 158 | 159 | -------------------------------------------------------------------------------- /nodes/mcu/digital/mcu_digital.js: -------------------------------------------------------------------------------- 1 | module.exports = function(RED) { 2 | function DigitalInNode(config) { 3 | RED.nodes.createNode(this, config); 4 | console.log(config) 5 | } 6 | RED.nodes.registerType("mcu_digital_in", DigitalInNode); 7 | function DigitalOutNode(config) { 8 | RED.nodes.createNode(this, config); 9 | console.log(config) 10 | } 11 | RED.nodes.registerType("mcu_digital_out", DigitalOutNode); 12 | } 13 | -------------------------------------------------------------------------------- /nodes/mcu/i2c/i2c.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | // modeled on https://flows.nodered.org/node/node-red-contrib-i2c 22 | // 23 | // one i2c instnace active at a time; could cache more than one 24 | // 25 | import {Node} from "nodered"; 26 | 27 | let cache; 28 | 29 | function getI2C(options, done) { 30 | const o = cache?.options; 31 | if (o && 32 | (o.bus === options.bus) && 33 | (o.clock === options.clock) && 34 | (o.data === options.data) && 35 | (o.hz === options.hz) && 36 | (o.address === options.address)) { 37 | return cache; 38 | } 39 | cache?.close(); 40 | cache = null; 41 | 42 | if (options.bus) { 43 | const o = globalThis.device.I2C?.[options.bus]; 44 | if (!o) return; 45 | 46 | cache = new (o.io)({ 47 | ...o, 48 | hz: options.hz, 49 | address: options.address 50 | }); 51 | } 52 | else { 53 | const I2C = globalThis.device?.io?.I2C; 54 | if (!I2C) return; 55 | 56 | cache = new I2C(options); 57 | } 58 | 59 | cache.options = options; 60 | 61 | return cache; 62 | } 63 | 64 | /* 65 | This node will request data from a given device. 66 | The address and command can both be set in the dialog screen or 67 | dynamically with msg.address and msg.command. 68 | This node outputs the result as a buffer in msg.payload and 69 | places the address in msg.address and command in msg.command. 70 | */ 71 | 72 | class I2CInNode extends Node { 73 | #options; 74 | #bytes; 75 | #command; 76 | 77 | onStart(config) { 78 | super.onStart(config); 79 | 80 | this.#options = config.options; 81 | this.#bytes = config.bytes; 82 | this.#command = config.command; 83 | } 84 | onMessage(msg, done) { 85 | let options = this.#options; 86 | if (undefined !== msg.address) { 87 | options = { 88 | ...options, 89 | address: msg.address 90 | }; 91 | } 92 | 93 | try { 94 | const i2c = getI2C(options); 95 | if (!i2c) 96 | return void done(); 97 | 98 | const command = this.#command ?? msg.command; 99 | if (undefined != command) { // null or undefined 100 | i2c.write(Uint8Array.of(command), false); 101 | msg.command = command; 102 | } 103 | 104 | msg.payload = new Uint8Array(i2c.read(this.#bytes)); 105 | msg.address = options.address; 106 | 107 | done(); 108 | this.status({fill: "green", shape: "dot", text: "node-red:common.status.connected"}); 109 | 110 | return msg; 111 | } 112 | catch (e) { 113 | done(e); 114 | this.status({fill: "red", shape: "ring", text: "node-red:common.status.error"}); 115 | } 116 | } 117 | 118 | static type = "mcu_i2c_in"; 119 | static { 120 | RED.nodes.registerType(this.type, this); 121 | } 122 | } 123 | 124 | /* 125 | This node will send a given String/array/buffer to a given device. 126 | The address and command can both be set in the dialog screen or 127 | dynamically with msg.address and msg.command. The payload can be 128 | set statically or dynamically (using msg.payload). 129 | 130 | This payload can be a Buffer, Array, String or Integer. When you use integers 131 | the number of bytes to send is important and can be set between 0 and 31 bytes. 132 | 133 | Since v0.5.0 - you can daisychain this node, the input msg is sent unchanged to the next node. 134 | */ 135 | 136 | class I2COutNode extends Node { 137 | #options; 138 | #bytes; 139 | #command; 140 | #getter; 141 | 142 | onStart(config) { 143 | super.onStart(config); 144 | 145 | this.#options = config.options; 146 | this.#bytes = config.bytes; 147 | this.#command = config.command; 148 | this.#getter = config.getter; 149 | } 150 | onMessage(msg, done) { 151 | let options = this.#options; 152 | if (undefined !== msg.address) { 153 | options = { 154 | ...options, 155 | address: msg.address 156 | }; 157 | } 158 | 159 | try { 160 | const i2c = getI2C(options); 161 | if (!i2c) 162 | return void done(); 163 | 164 | const command = this.#command ?? msg.command; 165 | if (undefined != command) // null or undefined 166 | i2c.write(Uint8Array.of(command), false); 167 | 168 | let payload = this.#getter(msg); 169 | switch (typeof payload) { 170 | case "number": { 171 | const buffer = new Uint8Array(this.#bytes); 172 | let i = 0; 173 | payload = payload | 0; 174 | while (payload) { 175 | buffer[i++] = payload; 176 | payload >>= 8; 177 | } 178 | i2c.write(buffer); 179 | } break; 180 | case "string": 181 | i2c.write(ArrayBuffer.fromString(payload)); 182 | break; 183 | case "object": 184 | if (Array.isArray(payload)) 185 | i2c.write(Uint8Array.from(payload)); 186 | else if (payload instanceof Uint8Array) 187 | i2c.write(payload); 188 | break; 189 | } 190 | 191 | done(); 192 | this.status({fill: "green", shape: "dot", text: "node-red:common.status.connected"}); 193 | 194 | return msg; 195 | } 196 | catch (e) { 197 | done(e); 198 | this.status({fill: "red", shape: "ring", text: "node-red:common.status.error"}); 199 | } 200 | } 201 | 202 | static type = "mcu_i2c_out"; 203 | static { 204 | RED.nodes.registerType(this.type, this); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /nodes/mcu/i2c/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "mcu_i2c": "./i2c" 4 | }, 5 | "preload": "mcu_i2c" 6 | } 7 | -------------------------------------------------------------------------------- /nodes/mcu/i2c/mcu_i2c.js: -------------------------------------------------------------------------------- 1 | module.exports = function(RED) { 2 | function I2CInNode(config) { 3 | RED.nodes.createNode(this, config); 4 | console.log(config) 5 | } 6 | RED.nodes.registerType("mcu_i2c_in", I2CInNode); 7 | function I2COutNode(config) { 8 | RED.nodes.createNode(this, config); 9 | console.log(config) 10 | } 11 | RED.nodes.registerType("mcu_i2c_out", I2COutNode); 12 | } 13 | -------------------------------------------------------------------------------- /nodes/mcu/neopixels/colors.js: -------------------------------------------------------------------------------- 1 | // from node-red-nodes/hardware/neopixel/colours.js 2 | 3 | const list = [ 4 | 'aqua', '#00FFFF', 5 | 'aliceblue', '#F0F8FF', 6 | 'antiquewhite', '#FAEBD7', 7 | 'black', '#000000', 8 | 'off', '#000000', 9 | 'blue', '#0000FF', 10 | 'cyan', '#00FFFF', 11 | 'darkblue', '#00008B', 12 | 'darkcyan', '#008B8B', 13 | 'darkgreen', '#006400', 14 | 'darkturquoise', '#00CED1', 15 | 'deepskyblue', '#00BFFF', 16 | 'green', '#008000', 17 | 'lime', '#00FF00', 18 | 'mediumblue', '#0000CD', 19 | 'mediumspringgreen', '#00FA9A', 20 | 'navy', '#000080', 21 | 'springgreen', '#00FF7F', 22 | 'teal', '#008080', 23 | 'midnightblue', '#191970', 24 | 'dodgerblue', '#1E90FF', 25 | 'lightseagreen', '#20B2AA', 26 | 'forestgreen', '#228B22', 27 | 'seagreen', '#2E8B57', 28 | 'darkslategray', '#2F4F4F', 29 | 'darkslategrey', '#2F4F4F', 30 | 'limegreen', '#32CD32', 31 | 'mediumseagreen', '#3CB371', 32 | 'turquoise', '#40E0D0', 33 | 'royalblue', '#4169E1', 34 | 'steelblue', '#4682B4', 35 | 'darkslateblue', '#483D8B', 36 | 'mediumturquoise', '#48D1CC', 37 | 'indigo', '#4B0082', 38 | 'darkolivegreen', '#556B2F', 39 | 'cadetblue', '#5F9EA0', 40 | 'cornflowerblue', '#6495ED', 41 | 'mediumaquamarine', '#66CDAA', 42 | 'dimgray', '#696969', 43 | 'dimgrey', '#696969', 44 | 'slateblue', '#6A5ACD', 45 | 'olivedrab', '#6B8E23', 46 | 'slategray', '#708090', 47 | 'slategrey', '#708090', 48 | 'lightslategray', '#778899', 49 | 'lightslategrey', '#778899', 50 | 'mediumslateblue', '#7B68EE', 51 | 'lawngreen', '#7CFC00', 52 | 'aquamarine', '#7FFFD4', 53 | 'chartreuse', '#7FFF00', 54 | 'gray', '#808080', 55 | 'grey', '#808080', 56 | 'maroon', '#800000', 57 | 'olive', '#808000', 58 | 'purple', '#800080', 59 | 'lightskyblue', '#87CEFA', 60 | 'skyblue', '#87CEEB', 61 | 'blueviolet', '#8A2BE2', 62 | 'darkmagenta', '#8B008B', 63 | 'darkred', '#8B0000', 64 | 'saddlebrown', '#8B4513', 65 | 'darkseagreen', '#8FBC8F', 66 | 'lightgreen', '#90EE90', 67 | 'mediumpurple', '#9370DB', 68 | 'darkviolet', '#9400D3', 69 | 'palegreen', '#98FB98', 70 | 'darkorchid', '#9932CC', 71 | 'yellowgreen', '#9ACD32', 72 | 'sienna', '#A0522D', 73 | 'brown', '#A52A2A', 74 | 'darkgray', '#A9A9A9', 75 | 'darkgrey', '#A9A9A9', 76 | 'greenyellow', '#ADFF2F', 77 | 'lightblue', '#ADD8E6', 78 | 'paleturquoise', '#AFEEEE', 79 | 'lightsteelblue', '#B0C4DE', 80 | 'powderblue', '#B0E0E6', 81 | 'firebrick', '#B22222', 82 | 'darkgoldenrod', '#B8860B', 83 | 'mediumorchid', '#BA55D3', 84 | 'rosybrown', '#BC8F8F', 85 | 'darkkhaki', '#BDB76B', 86 | 'silver', '#C0C0C0', 87 | 'mediumvioletred', '#C71585', 88 | 'indianred', '#CD5C5C', 89 | 'peru', '#CD853F', 90 | 'chocolate', '#D2691E', 91 | 'tan', '#D2B48C', 92 | 'lightgray', '#D3D3D3', 93 | 'lightgrey', '#D3D3D3', 94 | 'thistle', '#D8BFD8', 95 | 'goldenrod', '#DAA520', 96 | 'orchid', '#DA70D6', 97 | 'palevioletred', '#DB7093', 98 | 'crimson', '#DC143C', 99 | 'gainsboro', '#DCDCDC', 100 | 'plum', '#DDA0DD', 101 | 'burlywood', '#DEB887', 102 | 'lightcyan', '#E0FFFF', 103 | 'lavender', '#E6E6FA', 104 | 'darksalmon', '#E9967A', 105 | 'palegoldenrod', '#EEE8AA', 106 | 'violet', '#EE82EE', 107 | 'azure', '#F0FFFF', 108 | 'honeydew', '#F0FFF0', 109 | 'khaki', '#F0E68C', 110 | 'lightcoral', '#F08080', 111 | 'sandybrown', '#F4A460', 112 | 'beige', '#F5F5DC', 113 | 'mintcream', '#F5FFFA', 114 | 'wheat', '#F5DEB3', 115 | 'whitesmoke', '#F5F5F5', 116 | 'ghostwhite', '#F8F8FF', 117 | 'lightgoldenrodyellow', '#FAFAD2', 118 | 'linen', '#FAF0E6', 119 | 'salmon', '#FA8072', 120 | 'oldlace', '#FDF5E6', 121 | 'warmwhite', '#FDF5E6', 122 | 'bisque', '#FFE4C4', 123 | 'blanchedalmond', '#FFEBCD', 124 | 'coral', '#FF7F50', 125 | 'cornsilk', '#FFF8DC', 126 | 'darkorange', '#FF8C00', 127 | 'deeppink', '#FF1493', 128 | 'floralwhite', '#FFFAF0', 129 | 'fuchsia', '#FF00FF', 130 | 'gold', '#FFD700', 131 | 'hotpink', '#FF69B4', 132 | 'ivory', '#FFFFF0', 133 | 'lavenderblush', '#FFF0F5', 134 | 'lemonchiffon', '#FFFACD', 135 | 'lightpink', '#FFB6C1', 136 | 'lightsalmon', '#FFA07A', 137 | 'lightyellow', '#FFFFE0', 138 | 'magenta', '#FF00FF', 139 | 'mistyrose', '#FFE4E1', 140 | 'moccasin', '#FFE4B5', 141 | 'navajowhite', '#FFDEAD', 142 | 'orange', '#FFA500', 143 | 'orangered', '#FF4500', 144 | 'papayawhip', '#FFEFD5', 145 | 'peachpuff', '#FFDAB9', 146 | 'pink', '#FFC0CB', 147 | 'red', '#FF0000', 148 | 'seashell', '#FFF5EE', 149 | 'snow', '#FFFAFA', 150 | 'tomato', '#FF6347', 151 | 'white', '#FFFFFF', 152 | 'yellow', '#FFFF00', 153 | 'amber', '#FFD200' 154 | ]; 155 | 156 | const colors = new Map; 157 | for (let i = 0; i < list.length; i += 2) { 158 | const name = list[i], value = list[i + 1]; 159 | const r = parseInt(value.slice(1,3), 16); 160 | const g = parseInt(value.slice(3,5), 16); 161 | const b = parseInt(value.slice(5), 16); 162 | colors.set(name, (r << 16) | (g << 8) | b); 163 | } 164 | 165 | const p1 = /^\#[a-f0-9]{6}$/; 166 | 167 | function getRGB(col, result = {}) { 168 | col = col.toString().toLowerCase(); 169 | let value = colors.get(col); 170 | if (undefined !== value) { 171 | result.r = (value >> 16) & 0xff; 172 | result.g = (value >> 8) & 0xff; 173 | result.b = value & 0xff; 174 | return result; 175 | } 176 | 177 | if (p1.test(col)) { 178 | return { 179 | r: parseInt(col.slice(1,3), 16), 180 | g: parseInt(col.slice(3,5), 16), 181 | b: parseInt(col.slice(5), 16) 182 | }; 183 | } 184 | } 185 | 186 | export default Object.freeze({ 187 | getRGB 188 | }); 189 | -------------------------------------------------------------------------------- /nodes/mcu/neopixels/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "*": [ 4 | "./colors" 5 | ] 6 | }, 7 | "preload": [ 8 | "colors" 9 | ], 10 | "platforms": { 11 | "esp32": { 12 | "include": "$(MODULES)/drivers/neopixel/manifest.json", 13 | "modules": { 14 | "*": "./neopixels" 15 | }, 16 | "preload": "neopixels" 17 | }, 18 | "pico": { 19 | "include": "$(MODULES)/drivers/neopixel/manifest.json", 20 | "modules": { 21 | "*": "./neopixels" 22 | }, 23 | "preload": "neopixels" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /nodes/mcu/neopixels/mcu_neopixels.html: -------------------------------------------------------------------------------- 1 | 43 | 44 | 101 | 102 | 103 | 104 | 132 | -------------------------------------------------------------------------------- /nodes/mcu/neopixels/mcu_neopixels.js: -------------------------------------------------------------------------------- 1 | module.exports = function(RED) { 2 | function NeoPixelsNode(config) { 3 | RED.nodes.createNode(this,config); 4 | console.log(config) 5 | } 6 | RED.nodes.registerType("mcu_neopixels", NeoPixelsNode); 7 | } 8 | -------------------------------------------------------------------------------- /nodes/mcu/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@moddable-node-red/mcu", 3 | "version": "1.0.4", 4 | "description": "A suite of IO nodes for Node-RED MCU Edition", 5 | "scripts": { 6 | "test": "echo \"Error: no tests\" && exit 1" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/phoddie/node-red-mcu.git", 11 | "directory": "nodes/mcu" 12 | }, 13 | "keywords": [ 14 | "mcu", 15 | "node-red", 16 | "node-red-mcu", 17 | "ECMA-419", 18 | "IO", 19 | "Analog", 20 | "Digital", 21 | "I2C", 22 | "NeoPixels", 23 | "PulseCount", 24 | "PulseWidth", 25 | "PWM", 26 | "RTC", 27 | "Clock", 28 | "Sensor" 29 | ], 30 | "author": "Peter Hoddie, Patrick Soquet", 31 | "license": "LGPL-3.0-or-later", 32 | "bugs": { 33 | "url": "https://github.com/phoddie/node-red-mcu/issues" 34 | }, 35 | "homepage": "https://github.com/phoddie/node-red-mcu/nodes/mcu/readme.md", 36 | "engines": { 37 | "node": ">=14.0.0" 38 | }, 39 | "node-red": { 40 | "version": ">=3.0.0", 41 | "nodes": { 42 | "mcu_analog": "analog/mcu_analog.js", 43 | "mcu_clock": "clock/mcu_clock.js", 44 | "mcu_digital": "digital/mcu_digital.js", 45 | "mcu_i2c": "i2c/mcu_i2c.js", 46 | "mcu_neopixels": "neopixels/mcu_neopixels.js", 47 | "mcu_pulsecount": "pulsecount/mcu_pulsecount.js", 48 | "mcu_pulsewidth": "pulsewidth/mcu_pulsewidth.js", 49 | "mcu_pwm": "pwm/mcu_pwm.js", 50 | "mcu_sensor": "sensor/mcu_sensor.js" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /nodes/mcu/pulsecount/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "*": [ 4 | "./pulsecount" 5 | ] 6 | }, 7 | "preload": [ 8 | "pulsecount" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /nodes/mcu/pulsecount/mcu_pulsecount.html: -------------------------------------------------------------------------------- 1 | 28 | 29 | 42 | 43 | 44 | 45 | 54 | -------------------------------------------------------------------------------- /nodes/mcu/pulsecount/mcu_pulsecount.js: -------------------------------------------------------------------------------- 1 | module.exports = function(RED) { 2 | function PulseCountNode(config) { 3 | RED.nodes.createNode(this, config); 4 | console.log(config) 5 | } 6 | RED.nodes.registerType("mcu_pulse_count", PulseCountNode); 7 | } 8 | -------------------------------------------------------------------------------- /nodes/mcu/pulsecount/pulsecount.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022-2023 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | import {Node} from "nodered"; 22 | 23 | class PulseCountNode extends Node { 24 | onStart(config) { 25 | super.onStart(config); 26 | 27 | if (!globalThis.device?.io?.PulseCount) 28 | return void this.status({fill: "red", shape: "dot", text: "node-red:common.status.error"}); 29 | 30 | try { 31 | const io = new device.io.PulseCount({ 32 | signal: config.signal, 33 | control: config.control, 34 | onReadable: () => this.send({payload: io.read()}) 35 | }); 36 | } 37 | catch { 38 | this.status({fill: "red", shape: "dot", text: "node-red:common.status.error"}); 39 | } 40 | } 41 | 42 | static type = "mcu_pulse_count"; 43 | static { 44 | RED.nodes.registerType(this.type, this); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /nodes/mcu/pulsewidth/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "*": [ 4 | "./pulsewidth" 5 | ] 6 | }, 7 | "preload": [ 8 | "pulsewidth" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /nodes/mcu/pulsewidth/mcu_pulsewidth.html: -------------------------------------------------------------------------------- 1 | 29 | 30 | 52 | 53 | 54 | 55 | 64 | -------------------------------------------------------------------------------- /nodes/mcu/pulsewidth/mcu_pulsewidth.js: -------------------------------------------------------------------------------- 1 | module.exports = function(RED) { 2 | function PulseWidthNode(config) { 3 | RED.nodes.createNode(this, config); 4 | console.log(config) 5 | } 6 | RED.nodes.registerType("mcu_pulse_width", PulseWidthNode); 7 | } 8 | -------------------------------------------------------------------------------- /nodes/mcu/pulsewidth/pulsewidth.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022-2023 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | import {Node} from "nodered"; 22 | 23 | class PulseWidthInNode extends Node { 24 | onStart(config) { 25 | super.onStart(config); 26 | 27 | const PulseWidth = globalThis.device?.io?.PulseWidth; 28 | if (!PulseWidth) 29 | return void this.status({fill: "red", shape: "dot", text: "node-red:common.status.error"}); 30 | 31 | const node = this; 32 | new PulseWidth({ 33 | pin: config.pin, 34 | mode: PulseWidth[config.mode], 35 | edges: PulseWidth[config.edges], 36 | onReadable() { 37 | node.send({payload: this.read()}); 38 | } 39 | }); 40 | } 41 | 42 | static type = "mcu_pulse_width"; 43 | static { 44 | RED.nodes.registerType(this.type, this); 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /nodes/mcu/pwm/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "*": [ 4 | "./pwm" 5 | ] 6 | }, 7 | "preload": [ 8 | "pwm" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /nodes/mcu/pwm/mcu_pwm.html: -------------------------------------------------------------------------------- 1 | 28 | 29 | 42 | 43 | 44 | 45 | 54 | -------------------------------------------------------------------------------- /nodes/mcu/pwm/mcu_pwm.js: -------------------------------------------------------------------------------- 1 | module.exports = function(RED) { 2 | function PWMOutNode(config) { 3 | RED.nodes.createNode(this, config); 4 | console.log(config) 5 | } 6 | RED.nodes.registerType("mcu_pwm_out", PWMOutNode); 7 | } 8 | -------------------------------------------------------------------------------- /nodes/mcu/pwm/pwm.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022-2023 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | import {Node} from "nodered"; 22 | 23 | let cache; // support multiple nodes sharing the same pin, like the RPi implementation 24 | 25 | class PWMOutNode extends Node { 26 | #io; 27 | 28 | onStart(config) { 29 | super.onStart(config); 30 | 31 | if (!globalThis.device?.io?.PWM) 32 | return void this.status({fill: "red", shape: "dot", text: "node-red:common.status.error"}); 33 | 34 | cache ??= new Map; 35 | let io = cache.get(config.pin); 36 | 37 | if (io) { 38 | this.#io = io; 39 | } 40 | else { 41 | try { 42 | const options = { 43 | pin: config.pin, 44 | }; 45 | if (config.hz) 46 | options.hz = config.hz; 47 | this.#io = io = new device.io.PWM(options); 48 | cache.set(config.pin, io); 49 | } 50 | catch { 51 | this.status({fill: "red", shape: "dot", text: "node-red:common.status.error"}); 52 | } 53 | } 54 | } 55 | onMessage(msg, done) { 56 | if (this.#io) { 57 | this.#io.write(msg.payload * ((1 << this.#io.resolution) - 1)); 58 | this.status({fill:"green", shape:"dot", text: msg.payload.toString()}); 59 | } 60 | done(); 61 | } 62 | 63 | static type = "mcu_pwm_out"; 64 | static { 65 | RED.nodes.registerType(this.type, this); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /nodes/mcu/readme.md: -------------------------------------------------------------------------------- 1 | # MCU Nodes 2 | Copyright 2022-2023, Moddable Tech, Inc. All rights reserved.
3 | Peter Hoddie
4 | Updated April 30, 2023
5 | 6 | The MCU Nodes are a suite of nodes that provides access to features of microcontrollers including various I/O methods, Neopixels light strips, real-time clocks, and sensors. Each node includes built-in documentation describing their inputs, outputs, and configuration options. 7 | 8 | The following nodes are included in the MCU Node suite: 9 | 10 | - Analog 11 | - Digital Input 12 | - Digital Output 13 | - Real-time Clock 14 | - I²C Read 15 | - I²C Write 16 | - Neopixels 17 | - Pulse Count 18 | - Pulse Width Input 19 | - PWM Output 20 | - Sensor 21 | 22 | The MCU Nodes appear in the MCU section of the Node-RED Editor's palette. 23 | 24 | 25 | 26 | Here's a flow using Sensor node to log periodic readings from a temperature sensor to the debug console. 27 | 28 | 29 | 30 | Nearly all the MCU nodes are implemented using the [ECMA-419 standard](https://419.ecma-international.org), the ECMAScript embedded systems API specification. This provides a rich set of features that run on a variety of microcontrollers. No knowledge of ECMA-419 is required to use the MCU nodes. 31 | 32 | > **Note**: Node-RED MCU Edition also provides limited support for some Raspberry Pi I/O modules, including rpi-gpio, rpi-neopixels, and rpi-i2c. These are provided for projects that need to run on both MCUs and Raspberry Pi. They are implemented using the MCU Nodes. For projects intended to be used only with Node-RED MCU Edition, the MCU Nodes are preferred. 33 | -------------------------------------------------------------------------------- /nodes/mcu/sensor/icons/mcu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoddie/node-red-mcu/f17e3abc7b9658a3b415db56e5bcb3d80498ce5f/nodes/mcu/sensor/icons/mcu.png -------------------------------------------------------------------------------- /nodes/mcu/sensor/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "sensor": "./sensor" 4 | }, 5 | "preload": "sensor" 6 | } 7 | -------------------------------------------------------------------------------- /nodes/mcu/sensor/mcu_sensor.html: -------------------------------------------------------------------------------- 1 | 132 | 133 | 165 | 166 | 167 | 168 | 187 | -------------------------------------------------------------------------------- /nodes/mcu/sensor/mcu_sensor.js: -------------------------------------------------------------------------------- 1 | const Compounds = { 2 | Accelerometer: "accelerometer", 3 | AmbientLight: "lightmeter", 4 | AtmosphericPressure: "barometer", 5 | Barometer: "barometer", 6 | Gyroscope: "gyroscope", 7 | Humidity: "hygrometer", 8 | Magnetometer: "magnetometer", 9 | Proximity: "proximity", 10 | Temperature: "thermometer", 11 | Touch: "touch" 12 | }; 13 | 14 | module.exports = function(RED) { 15 | function SensorNode(config) { 16 | RED.nodes.createNode(this,config); 17 | console.log(config) 18 | let sensors; 19 | const prefix = "embedded:sensor/"; // embedded:sensor/AtmosphericPressure-Temperature/BMP180 20 | if (config.module.startsWith(prefix)) { 21 | sensors = config.module.substring(prefix.length).split("/")[0]; 22 | if (sensors) 23 | sensors = sensors.split("-"); 24 | } 25 | if (!sensors || !sensors.length) 26 | sensors = ["Temperature"]; 27 | this.on('input', (msg, send, done) => { 28 | if (1 === sensors.length) { 29 | msg.payload = simulateOne(sensors[0]); 30 | } 31 | else { 32 | msg.payload = {}; 33 | sensors.forEach(sensor => { 34 | if (Compounds[sensor]) 35 | msg.payload[Compounds[sensor]] = simulateOne(sensor); 36 | }); 37 | } 38 | 39 | msg.payload.simulated = true; 40 | send(msg); 41 | done(); 42 | }); 43 | } 44 | RED.nodes.registerType("sensor", SensorNode); 45 | } 46 | 47 | function simulateOne(sensor) { 48 | const sample = {}; 49 | 50 | switch (sensor) { 51 | case "Accelerometer": 52 | sample.x = 1 - (Math.random() * 2); 53 | sample.y = 1 - (Math.random() * 2); 54 | sample.z = 1 - (Math.random() * 2); 55 | break; 56 | case "AmbientLight": 57 | sample.illuminance = (Math.random() * 5500) + 20; 58 | break; 59 | case "AtmosphericPressure": 60 | case "Barometer": 61 | sample.pressure = 101_325 + (10_000 - (Math.random() * 20_000)); 62 | break; 63 | case "Gyroscope": 64 | sample.x = 1 - (Math.random() * 2); 65 | sample.y = 1 - (Math.random() * 2); 66 | sample.z = 1 - (Math.random() * 2); 67 | break; 68 | case "Proximity": 69 | sample.max = 1000; 70 | sample.distance = Math.random() * sample.max; 71 | sample.near = sample.distance < 50; 72 | break; 73 | case "Temperature": 74 | sample.temperature = 22.5 - (Math.random() * 5) 75 | break; 76 | case "Touch": 77 | sample.x = Math.round(Math.random() * 240); 78 | sample.y = Math.round(Math.random() * 320); 79 | break; 80 | } 81 | 82 | return sample; 83 | } 84 | 85 | -------------------------------------------------------------------------------- /nodes/mcu/sensor/sensor.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022-2023 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | import {Node} from "nodered"; 22 | 23 | class Sensor extends Node { 24 | #sensor; 25 | 26 | onStart(config) { 27 | super.onStart(config); 28 | 29 | try { 30 | this.#sensor = config.initialize.call(this); 31 | this.status({fill: "green", shape: "dot", text: "node-red:common.status.connected"}); 32 | 33 | if (config.asyncSample) { 34 | this.#sensor.callback = (error, sample) => { 35 | const sensor = this.#sensor; 36 | const pending = sensor.pending.shift(); 37 | if (!sensor.pending.length) 38 | delete sensor.pending; 39 | 40 | if (error) { 41 | sensor.close(); 42 | this.#sensor = undefined; 43 | this.status({fill: "red", shape: "ring", text: "node-red:common.status.disconnected"}); 44 | pending.done(error); 45 | } 46 | else if (sample) { 47 | pending.msg.payload = sample; 48 | this.send(pending.msg); 49 | pending.done(); 50 | } 51 | } 52 | } 53 | } 54 | catch { 55 | this.status({fill: "red", shape: "ring", text: "node-red:common.status.disconnected"}); 56 | } 57 | } 58 | onMessage(msg, done) { 59 | const sensor = this.#sensor; 60 | if (!sensor) 61 | return; 62 | 63 | if (msg.configuration) { 64 | try { 65 | sensor.configure(msg.configuration); 66 | done?.(); 67 | } 68 | catch (e) { 69 | done?.(e); 70 | } 71 | return; 72 | } 73 | 74 | try { 75 | if (sensor.callback) { 76 | sensor.sample(sensor.callback); 77 | sensor.pending ??= []; 78 | sensor.pending.push({done, msg}); 79 | return; 80 | } 81 | 82 | const payload = sensor.sample(); 83 | if (payload) 84 | msg.payload = payload; 85 | else 86 | msg = undefined; 87 | } 88 | catch { 89 | this.status({fill: "red", shape: "ring", text: "node-red:common.status.disconnected"}); 90 | sensor.close(); 91 | this.#sensor = undefined; 92 | msg = undefined; 93 | } 94 | finally { 95 | done?.(); 96 | } 97 | 98 | return msg; 99 | } 100 | 101 | static type = "sensor"; 102 | static { 103 | RED.nodes.registerType(this.type, this); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /nodes/network/httprequest/httprequest.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022-2023 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | import {Node} from "nodered"; 22 | import fetch from "fetch"; 23 | import {Headers} from "fetch"; 24 | import {URLSearchParams} from "url"; 25 | import Mustache from "mustache"; 26 | import CertificateManager from "ssl/cert"; 27 | 28 | class HTTPRequestNode extends Node { 29 | #options; 30 | #format; 31 | #paytoqs; 32 | #persist; 33 | 34 | onStart(config) { 35 | super.onStart(config); 36 | 37 | if (config.proxy || config.authType /* || config.senderr */) 38 | throw new Error("unimplemented"); 39 | 40 | this.#format = config.ret; 41 | this.#paytoqs = config.paytoqs; 42 | this.#persist = config.persist; 43 | this.#options = { 44 | method: config.method, 45 | url: config.url 46 | }; 47 | if (config.tls) 48 | this.#options.tls = RED.nodes.getNode(config.tls); 49 | } 50 | onMessage(msg) { 51 | const headers = new Headers([ 52 | ["User-Agent", "node-red-mcu/v0"] 53 | ]); 54 | headers.set("Connection", this.#persist ? "keep-alive" : "close"); //@@ not sure 55 | for (let name in msg.headers) 56 | headers.set(name, msg.headers[name]); 57 | // let body = ("ignore" === this.#paytoqs) ? undefined : msg.payload; //@@ what does paytoqs do?? 58 | let body = msg.payload; 59 | let url = msg.url; 60 | if (this.#options.url) { 61 | url = this.#options.url; 62 | if (url.indexOf("{{") >= 0) 63 | url = Mustache.render(url, msg) 64 | } 65 | const options = { 66 | method: msg.method ?? this.#options.method ?? "GET", 67 | headers 68 | } 69 | if (undefined !== body) { 70 | if ("object" === typeof body) { 71 | if (!(body instanceof ArrayBuffer)) { 72 | if ("query" === this.#paytoqs) { 73 | url += (url.indexOf("?") < 0) ? "?" : "&"; 74 | url += (new URLSearchParams(Object.entries(body))).toString(); 75 | } 76 | else { 77 | body = JSON.stringify(body); 78 | headers.set("content-type", "application/json"); 79 | } 80 | } 81 | } 82 | else 83 | body = body.toString(); 84 | 85 | options.body = body; 86 | } 87 | 88 | if (!url.startsWith("http:") && !url.startsWith("https:")) 89 | url = (this.#options.tls ? "https://" : "http://") + url; 90 | 91 | if (url.startsWith("https://")) { 92 | if (false === this.#options.tls?.options?.verifyservercert) 93 | throw new Error("cannot skip server cert validation yet") 94 | 95 | const ca = this.#options.tls?.options?.ca; 96 | if (ca) 97 | CertificateManager.register(ca); 98 | } 99 | 100 | fetch(url, options) 101 | .then(response => { 102 | msg.statusCode = response.status; 103 | msg.headers = {/* ["x-node-red-request-node"]: this.id */}; //@@ what is this? 104 | response.headers.forEach((value, key) => {msg.headers[key] = value;}); 105 | 106 | if (("txt" === this.#format) || ("obj" === this.#format)) 107 | return response.text(); 108 | if ("bin" === this.#format) 109 | return response.arrayBuffer(); 110 | throw new Error("unexpected http request format"); 111 | }) 112 | .then(payload => { 113 | msg.payload = payload; 114 | if ("obj" === this.#format) { 115 | try { 116 | msg.payload = JSON.parse(payload); 117 | } 118 | catch { // if parse fails, node sends unparsed text 119 | } 120 | } 121 | this.send(msg); 122 | }) 123 | .catch(e => { 124 | this.send(msg); 125 | }); 126 | } 127 | 128 | static type = "http request"; 129 | static { 130 | RED.nodes.registerType(this.type, this); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /nodes/network/httprequest/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/io/tcp/fetch/manifest_fetch.json" 4 | ], 5 | "modules": { 6 | "*": [ 7 | "./httprequest", 8 | "../../function/template/mustache" 9 | ] 10 | }, 11 | "preload": [ 12 | "httprequest", 13 | "mustache" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /nodes/network/httpserver/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/io/listener/httpserver/manifest_httpserver.json", 4 | "$(MODDABLE)/modules/data/text/decoder/manifest.json", 5 | "$(MODDABLE)/modules/data/text/encoder/manifest.json" 6 | ], 7 | "modules": { 8 | "*": "./httpserver" 9 | }, 10 | "preload": "httpserver" 11 | } 12 | -------------------------------------------------------------------------------- /nodes/network/mqtt/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/modules/data/base64/manifest.json", 4 | "$(MODDABLE)/examples/io/tcp/mqttclient/manifest_mqttclient.json" 5 | ], 6 | "modules": { 7 | "*": "./mqttnodes" 8 | }, 9 | "preload": "mqttnodes" 10 | } 11 | -------------------------------------------------------------------------------- /nodes/network/tcp/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODULES)/data/text/decoder/manifest.json" 4 | ], 5 | "modules": { 6 | "*": "./tcpnodes" 7 | }, 8 | "preload": "tcpnodes" 9 | } 10 | -------------------------------------------------------------------------------- /nodes/network/tls-config/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/io/tcp/tlssocket/manifest.json" 4 | ], 5 | "modules": { 6 | "*": "./tls-config" 7 | }, 8 | "preload": "tls-config" 9 | } 10 | -------------------------------------------------------------------------------- /nodes/network/tls-config/tls-config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | import {Node} from "nodered"; 22 | import Resource from "Resource"; 23 | 24 | class TLSConfig extends Node { 25 | onStart(config) { 26 | super.onStart(config); 27 | 28 | if (Resource.exists(this.id +"-ca.der")) 29 | config.ca = new Resource(this.id +"-ca.der"); 30 | if (Resource.exists(this.id +"-cert.der")) 31 | config.cert = new Resource(this.id +"-cert.der"); 32 | if (Resource.exists(this.id +"-key.der")) 33 | config.key = new Resource(this.id +"-key.der"); 34 | 35 | Object.defineProperty(this, "options", { 36 | value: Object.freeze(config, true), 37 | enumerable: true 38 | }); 39 | } 40 | 41 | static type = "tls-config"; 42 | static { 43 | RED.nodes.registerType(this.type, this); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /nodes/network/udp/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODULES)/data/base64/manifest.json" 4 | ], 5 | "modules": { 6 | "*": "./udpnodes" 7 | }, 8 | "preload": "udpnodes" 9 | } 10 | -------------------------------------------------------------------------------- /nodes/network/udp/udpnodes.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | import UDP from "embedded:io/socket/udp"; 22 | import {Node} from "nodered"; 23 | import Base64 from "base64"; 24 | 25 | // all nodes bound to same port share a single UDP socket 26 | class SharedUDP { 27 | static sockets = []; 28 | 29 | static add(port, onReadable) { 30 | let socket = SharedUDP.sockets[port]; 31 | if (socket) { 32 | if (onReadable) { 33 | socket.nodes ??= []; 34 | socket.nodes.push(onReadable); 35 | } 36 | return socket; 37 | } 38 | 39 | const options = {onReadable: this.onReadable}; 40 | if (port) 41 | options.port = port; 42 | socket = new UDP(options); 43 | if (port) 44 | SharedUDP.sockets[port] = socket; 45 | if (onReadable) 46 | socket.nodes = [onReadable]; 47 | 48 | return socket; 49 | } 50 | static onReadable(count) { // called with this === udp socket 51 | while (count--) { 52 | const buffer = this.read(); 53 | this.nodes?.forEach(onReadable => onReadable(buffer)); 54 | } 55 | } 56 | } 57 | 58 | class UDPIn extends Node { 59 | onStart(config) { 60 | super.onStart(config); 61 | 62 | if (("udp4" !== config.ipv) || config.iface || ("false" !== config.multicast)) 63 | throw new Error("unsupported"); 64 | 65 | const datatype = config.datatype; 66 | SharedUDP.add(parseInt(config.port), buffer => { 67 | let payload; 68 | if ("utf8" === datatype) { 69 | try { 70 | payload = String.fromArrayBuffer(buffer); 71 | } 72 | catch { 73 | return; 74 | } 75 | } 76 | else if ("buffer" === datatype) 77 | payload = new Uint8Array(buffer); 78 | else if ("base64" === datatype) 79 | payload = Base64.encode(buffer); 80 | this.send({ 81 | payload, 82 | ip: buffer.address, 83 | port: buffer.port 84 | }); 85 | }); 86 | } 87 | 88 | static type = "udp in"; 89 | static { 90 | RED.nodes.registerType(this.type, this); 91 | } 92 | } 93 | 94 | class UDPOut extends Node { 95 | #socket; // could share across multiple instances 96 | #ip; 97 | #port; 98 | #base64; 99 | 100 | onStart(config) { 101 | super.onStart(config); 102 | 103 | if (("udp4" !== config.ipv) || config.iface || ("false" !== config.multicast)) 104 | throw new Error("unsupported"); 105 | 106 | if (config.addr) 107 | this.#ip = config.addr; 108 | if (config.port) 109 | this.#port = parseInt(config.port); 110 | this.#base64 = "true" === config.base64; 111 | 112 | this.#socket = SharedUDP.add(config.outport ? parseInt(config.outport) : 0); 113 | } 114 | onMessage(msg) { 115 | const ip = this.#ip ?? msg.ip; 116 | const port = this.#port ?? msg.port; 117 | let payload = msg.payload; 118 | 119 | if (!(payload instanceof Uint8Array)) { 120 | try { 121 | if (this.#base64) 122 | payload = Base64.decode(payload); 123 | else 124 | payload = ArrayBuffer.fromString(payload); 125 | } 126 | catch (e) { 127 | this.error(e); 128 | return; 129 | } 130 | } 131 | 132 | this.#socket.write(ip, port, payload); 133 | } 134 | 135 | static type = "udp out"; 136 | static { 137 | RED.nodes.registerType(this.type, this); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /nodes/network/websocketnodes/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/io/tcp/websocket/manifest_websocket.json" 4 | ], 5 | "modules": { 6 | "*": "./websocketnodes" 7 | }, 8 | "preload": "websocketnodes" 9 | } 10 | -------------------------------------------------------------------------------- /nodes/network/websocketnodes/websocketnodes.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022-2024 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | import {Node, configFlowID} from "nodered"; 22 | import Timer from "timer"; 23 | import WebSocket from "WebSocket"; 24 | import Modules from "modules"; 25 | 26 | const connected = Object.freeze({ 27 | fill: "green", 28 | shape: "dot", 29 | text: "connected ", 30 | event: "connect" 31 | }); 32 | 33 | const disconnected = Object.freeze({ 34 | fill: "red", 35 | shape: "ring", 36 | text: "common.status.disconnected", 37 | event: "disconnect" 38 | }); 39 | 40 | class WebSocketClient extends Node { 41 | #ws; 42 | #reconnect; 43 | #options; 44 | #nodes; 45 | #tls; 46 | 47 | onStart(config) { 48 | if (config.tls) 49 | this.#tls = RED.nodes.getNode(config.tls); 50 | 51 | this.#options = { 52 | path: config.path, 53 | wholemsg: "true" === config.wholemsg, 54 | subprotocol: config.subprotocol, 55 | keepalive: config.hb ? (1000 * parseInt(config.hb)) : 0, 56 | }; 57 | 58 | this.status({...disconnected}); 59 | 60 | Timer.set(() => this.#connect()); 61 | } 62 | onMessage(msg) { 63 | if (1 !== this.#ws?.readyState) 64 | return; 65 | 66 | if (this.#options.wholemsg) 67 | this.#ws.send(JSON.stringify(msg)); 68 | else 69 | this.#ws.send(msg.payload); 70 | } 71 | #connect() { 72 | let wss; 73 | if (this.#tls) { 74 | wss = { 75 | ...device.network.ws, 76 | socket: { 77 | io: Modules.importNow("embedded:io/socket/tcp/tls"), 78 | TCP: { 79 | io: device.network.ws.socket.io 80 | }, 81 | secure: { 82 | verify: this.#tls.options?.verifyservercert ?? true 83 | } 84 | } 85 | }; 86 | const servername = this.#tls.options?.servername; 87 | if (servername) wss.socket.secure.serverName = servername; 88 | const ca = this.#tls.options?.ca; 89 | if (ca) wss.socket.secure.certificate = ca; 90 | const cert = this.#tls.options?.cert; 91 | if (cert) { 92 | wss.socket.secure.clientCertificates = [cert]; 93 | const key = this.#tls.options?.key; 94 | if (key) wss.socket.secure.clientKey = key; 95 | } 96 | } 97 | 98 | const options = this.#options, o = {}; 99 | if (wss) o.wss = wss; 100 | ({path: o.url, subprotocol: o.subprotocol, keepalive: o.keepalive} = options); 101 | this.#ws = new WebSocket(o); 102 | this.#ws.binaryType = "arraybuffer"; 103 | this.#ws.addEventListener("open", () => { 104 | Timer.clear(this.#reconnect); 105 | this.#reconnect = undefined; 106 | this.status({...connected}); 107 | }); 108 | if (this.#nodes) { 109 | this.#ws.addEventListener("message", event => { 110 | let msg = event.data; 111 | if (this.#options.wholemsg) 112 | msg = JSON.parse(msg); 113 | else 114 | msg = {payload: msg}; 115 | 116 | for (let node of this.#nodes) 117 | node.send(msg); 118 | }); 119 | } 120 | const close = () => { 121 | this.#ws = undefined; 122 | this.status({...disconnected}); 123 | 124 | this.#reconnect ??= Timer.repeat(() => { 125 | if (this.#ws) 126 | return; 127 | 128 | this.#connect(); 129 | }, 5_000); 130 | } 131 | this.#ws.addEventListener("close", close); 132 | this.#ws.addEventListener("error", close); 133 | } 134 | add(node) { 135 | this.#nodes ??= new Set; 136 | this.#nodes.add(node); 137 | } 138 | status(status) { 139 | const nodes = this.#nodes; 140 | if (!nodes) return; 141 | for (let node of nodes) 142 | node.status(status); 143 | } 144 | 145 | static type = "websocket-client"; 146 | static { 147 | RED.nodes.registerType(this.type, this); 148 | } 149 | } 150 | 151 | class WebSocketListener extends Node { 152 | #wholemsg; 153 | #connections = new Map; // remote connections to this listener 154 | #nodes; // "websocket in" nodes using this listener 155 | 156 | onStart(config) { 157 | this.#wholemsg = "true" === config.wholemsg; 158 | 159 | const Server = Modules.importNow("httpserver"); // dynamic import so dependency on http server only if websocket listener is used 160 | const WebSocketHandshake = Modules.importNow("embedded:network/http/server/options/websocket"); 161 | 162 | Server.add("GET", config.path, this, { 163 | ...WebSocketHandshake, 164 | listener: this, 165 | onDone() { 166 | const listener = this.route.listener; 167 | const ws = new WebSocket({socket: this.detach()}); 168 | ws._session = RED.util.generateId(); 169 | listener.#connections.set(ws._session, ws); 170 | ws.addEventListener("message", function(event) { 171 | let msg = event.data; 172 | if (listener.#wholemsg) 173 | msg = JSON.parse(msg); 174 | else 175 | msg = {payload: msg}; 176 | msg._session = {type: "websocket", id: ws._session}; 177 | 178 | for (let node of listener.#nodes) 179 | node.send(msg); 180 | }); 181 | const remove = function() { 182 | listener.#connections.delete(ws._session); 183 | } 184 | ws.addEventListener("close", remove); 185 | ws.addEventListener("error", remove); 186 | } 187 | }); 188 | } 189 | onMessage(msg, done) { 190 | const _session = msg._session; 191 | delete msg._session; 192 | const payload = this.#wholemsg ? JSON.stringify(msg) : (Buffer.isBuffer(msg.payload) ? msg.payload : RED.util.ensureString(msg.payload)); 193 | if ("websocket" === _session?.type) { 194 | const connection = this.#connections.get(_session.id); 195 | if (connection) 196 | connection.send(payload); 197 | else 198 | this.warn("websocket session not found") 199 | } 200 | else { 201 | for (const [id, connection] of this.#connections) 202 | connection.send(payload); 203 | } 204 | done(); 205 | } 206 | add(node) { 207 | this.#nodes ??= new Set; 208 | this.#nodes.add(node); 209 | } 210 | 211 | static type = "websocket-listener"; 212 | static { 213 | RED.nodes.registerType(this.type, this); 214 | } 215 | } 216 | 217 | class WebSocketIn extends Node { 218 | onStart(config) { 219 | super.onStart(config); 220 | 221 | const ws = flows.get(configFlowID).getNode(config.client || config.server); 222 | ws?.add(this); 223 | } 224 | 225 | static type = "websocket in"; 226 | static { 227 | RED.nodes.registerType(this.type, this); 228 | } 229 | } 230 | 231 | class WebSocketOut extends Node { 232 | #ws; 233 | 234 | onStart(config) { 235 | super.onStart(config); 236 | 237 | this.#ws = flows.get(configFlowID).getNode(config.client || config.server); 238 | } 239 | onMessage(msg, done) { 240 | return this.#ws.onMessage(msg, done); // maybe unnecessary to use RED.mcu.enqueue here 241 | } 242 | 243 | static type = "websocket out"; 244 | static { 245 | RED.nodes.registerType(this.type, this); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /nodes/ota-update/examples/ota-pull.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "c5fcfc5d2cb51ebf", 4 | "type": "tab", 5 | "label": "OTA Pull", 6 | "disabled": false, 7 | "info": "", 8 | "env": [] 9 | }, 10 | { 11 | "id": "b0a1deea7bf0c600", 12 | "type": "ota-update", 13 | "z": "c5fcfc5d2cb51ebf", 14 | "name": "OTA", 15 | "path": "", 16 | "moddable_manifest": { 17 | "include": "$(NODEREDMCU)/nodes/ota-update/manifest.json" 18 | }, 19 | "x": 370, 20 | "y": 220, 21 | "wires": [ 22 | [ 23 | "b62d22f76d13a47e" 24 | ] 25 | ] 26 | }, 27 | { 28 | "id": "26a90fa3bf80cfa1", 29 | "type": "inject", 30 | "z": "c5fcfc5d2cb51ebf", 31 | "name": "Download URL", 32 | "props": [ 33 | { 34 | "p": "url", 35 | "v": "http://10.0.0.24/xs_esp32.bin", 36 | "vt": "str" 37 | } 38 | ], 39 | "repeat": "", 40 | "crontab": "", 41 | "once": true, 42 | "onceDelay": 0.1, 43 | "topic": "", 44 | "x": 200, 45 | "y": 220, 46 | "wires": [ 47 | [ 48 | "b0a1deea7bf0c600" 49 | ] 50 | ] 51 | }, 52 | { 53 | "id": "a0a3b6d772cd9e42", 54 | "type": "comment", 55 | "z": "c5fcfc5d2cb51ebf", 56 | "name": "OTA Pull - Download from URL", 57 | "info": "", 58 | "x": 170, 59 | "y": 140, 60 | "wires": [] 61 | }, 62 | { 63 | "id": "b62d22f76d13a47e", 64 | "type": "mcu_restart", 65 | "z": "c5fcfc5d2cb51ebf", 66 | "name": "Restart", 67 | "moddable_manifest": { 68 | "include": [ 69 | { 70 | "git": "https://github.com/phoddie/mcu_restart.git" 71 | } 72 | ] 73 | }, 74 | "x": 520, 75 | "y": 220, 76 | "wires": [] 77 | }, 78 | { 79 | "id": "d62ad0abfa4b375f", 80 | "type": "status", 81 | "z": "c5fcfc5d2cb51ebf", 82 | "name": "OTA status", 83 | "scope": [ 84 | "b0a1deea7bf0c600" 85 | ], 86 | "x": 260, 87 | "y": 340, 88 | "wires": [ 89 | [ 90 | "ed4ebb72a7791f8d" 91 | ] 92 | ] 93 | }, 94 | { 95 | "id": "ed4ebb72a7791f8d", 96 | "type": "debug", 97 | "z": "c5fcfc5d2cb51ebf", 98 | "name": "OTA log", 99 | "active": true, 100 | "tosidebar": true, 101 | "console": true, 102 | "tostatus": false, 103 | "complete": "payload", 104 | "targetType": "msg", 105 | "statusVal": "", 106 | "statusType": "auto", 107 | "x": 420, 108 | "y": 380, 109 | "wires": [] 110 | }, 111 | { 112 | "id": "94fbfd5bd65acf0a", 113 | "type": "catch", 114 | "z": "c5fcfc5d2cb51ebf", 115 | "name": "OTA Error", 116 | "scope": [ 117 | "b0a1deea7bf0c600" 118 | ], 119 | "uncaught": false, 120 | "x": 260, 121 | "y": 420, 122 | "wires": [ 123 | [ 124 | "ed4ebb72a7791f8d" 125 | ] 126 | ] 127 | } 128 | ] 129 | -------------------------------------------------------------------------------- /nodes/ota-update/examples/ota-push.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "c5fcfc5d2cb51ebf", 4 | "type": "tab", 5 | "label": "OTA Push", 6 | "disabled": false, 7 | "info": "", 8 | "env": [] 9 | }, 10 | { 11 | "id": "b0a1deea7bf0c600", 12 | "type": "ota-update", 13 | "z": "c5fcfc5d2cb51ebf", 14 | "name": "OTA", 15 | "path": "/ota/firmware", 16 | "moddable_manifest": { 17 | "include": "$(NODEREDMCU)/nodes/ota-update/manifest.json" 18 | }, 19 | "x": 290, 20 | "y": 200, 21 | "wires": [ 22 | [ 23 | "492672252c86057c" 24 | ] 25 | ] 26 | }, 27 | { 28 | "id": "a0a3b6d772cd9e42", 29 | "type": "comment", 30 | "z": "c5fcfc5d2cb51ebf", 31 | "name": "OTA Push - Upload to HTTP endpoint /ota/firmware", 32 | "info": "", 33 | "x": 240, 34 | "y": 120, 35 | "wires": [] 36 | }, 37 | { 38 | "id": "d62ad0abfa4b375f", 39 | "type": "status", 40 | "z": "c5fcfc5d2cb51ebf", 41 | "name": "OTA status", 42 | "scope": [ 43 | "b0a1deea7bf0c600" 44 | ], 45 | "x": 280, 46 | "y": 280, 47 | "wires": [ 48 | [ 49 | "ed4ebb72a7791f8d" 50 | ] 51 | ] 52 | }, 53 | { 54 | "id": "ed4ebb72a7791f8d", 55 | "type": "debug", 56 | "z": "c5fcfc5d2cb51ebf", 57 | "name": "OTA log", 58 | "active": true, 59 | "tosidebar": true, 60 | "console": true, 61 | "tostatus": false, 62 | "complete": "payload", 63 | "targetType": "msg", 64 | "statusVal": "", 65 | "statusType": "auto", 66 | "x": 440, 67 | "y": 320, 68 | "wires": [] 69 | }, 70 | { 71 | "id": "94fbfd5bd65acf0a", 72 | "type": "catch", 73 | "z": "c5fcfc5d2cb51ebf", 74 | "name": "OTA Error", 75 | "scope": [ 76 | "b0a1deea7bf0c600" 77 | ], 78 | "uncaught": false, 79 | "x": 280, 80 | "y": 360, 81 | "wires": [ 82 | [ 83 | "ed4ebb72a7791f8d" 84 | ] 85 | ] 86 | }, 87 | { 88 | "id": "492672252c86057c", 89 | "type": "mcu_restart", 90 | "z": "c5fcfc5d2cb51ebf", 91 | "name": "Restart", 92 | "moddable_manifest": { 93 | "include": [ 94 | { 95 | "git": "https://github.com/phoddie/mcu_restart.git" 96 | } 97 | ] 98 | }, 99 | "x": 440, 100 | "y": 200, 101 | "wires": [] 102 | } 103 | ] 104 | -------------------------------------------------------------------------------- /nodes/ota-update/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/modules/data/url/manifest.json", 4 | "$(MODDABLE)/examples/io/tcp/httpclient/manifest_httpclient.json", 5 | "$(NODEREDMCU)/nodes/network/httpserver/manifest.json" 6 | ], 7 | "modules": { 8 | "*": "./ota-update" 9 | }, 10 | "preload": [ 11 | "ota-update" 12 | ], 13 | "platforms": { 14 | "esp32": { 15 | "include": "$(MODDABLE)/build/devices/esp32/modules/ota/manifest.json" 16 | }, 17 | "...": { 18 | "warning": "OTA not supported" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /nodes/ota-update/mcu_ota-update.js: -------------------------------------------------------------------------------- 1 | module.exports = function(RED) { 2 | function OTAUpdateNode(config) { 3 | RED.nodes.createNode(this,config); 4 | 5 | this.on('input', (msg, send, done) => { 6 | done(); 7 | }); 8 | } 9 | RED.nodes.registerType("ota-update", OTAUpdateNode); 10 | } 11 | -------------------------------------------------------------------------------- /nodes/ota-update/ota-update.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | import {Node} from "nodered"; 22 | import Modules from "modules"; 23 | import URL from "url"; 24 | // runtime import of "httpserver" and "ota" 25 | 26 | // curl http://127.0.0.1/ota -X PUT -H "Content-Type: application/octet-stream" --data-binary '@xs_esp32.bin' 27 | 28 | class OTAUpdateNode extends Node { 29 | #client; 30 | #ota; 31 | 32 | onStart(config) { 33 | super.onStart(config); 34 | 35 | if (!Modules.has("ota")) 36 | return void this.status({fill: "red", shape: "ring", text: "OTA not supported"}); 37 | 38 | if (config.path) { 39 | if (!Modules.has("httpserver")) 40 | return void this.status({fill: "red", shape: "ring", text: "no httpserver!"}); 41 | 42 | const Server = Modules.importNow("httpserver"); 43 | Server.add("PUT", config.path.startsWith("/") ? config.path : ("/" + config.path), this, route); 44 | } 45 | } 46 | onMessage(msg, done) { 47 | if (!msg.url) 48 | return void done(); 49 | 50 | if (this.#client) { // OTA in progress: cancel and start new request 51 | this.#ota?.cancel(); 52 | this.#client.close(); 53 | this.#ota = this.#client = undefined; 54 | } 55 | 56 | const url = new URL(msg.url); 57 | if ("http:" !== url.protocol) 58 | return void done("http only"); 59 | const options = { 60 | ...device.network.http, 61 | host: url.hostname 62 | }; 63 | if (url.port) 64 | options.port = parseInt(url.port); 65 | this.#client = new device.network.http.io(options); 66 | const request = this.#client.request({ 67 | path: url.pathname, 68 | onHeaders(status, headers) { 69 | if (2 !== Math.idiv(status, 100)) 70 | return void done("request failed with status " + status); 71 | 72 | this.node.begin(headers, done); 73 | }, 74 | onReadable(count) { 75 | this.node.append(this, count); 76 | }, 77 | onDone(error) { 78 | if (this.node.end(error, done)) { 79 | msg.payload = "ota pull"; 80 | this.node.send(msg); 81 | } 82 | } 83 | }); 84 | request.node = this; 85 | this.status({fill: "yellow", shape: "ring", text: "node-red:common.status.connecting"}); 86 | } 87 | begin(headers, done) { 88 | if (this.#ota) 89 | return void done?.("OTA update already in progress"); 90 | 91 | this.status({fill: "green", shape: "dot", text: "node-red:common.status.connected"}); 92 | const total = headers.get("content-length"); 93 | if (undefined !== total) 94 | this.total = parseInt(total); 95 | this.received = 0; 96 | 97 | try { 98 | const OTA = Modules.importNow("ota"); 99 | this.#ota = new OTA 100 | } 101 | catch (e) { 102 | e = e.toString(); 103 | this.status({fill: "red", shape: "ring", text: e}); 104 | this.#client?.close(); 105 | this.#client = undefined; 106 | done?.(e); 107 | } 108 | } 109 | append(from, count) { 110 | try { 111 | const data = from.read(count); 112 | this.#ota?.write(data); 113 | } 114 | catch (e) { // probably invalid OTA data 115 | this.#ota.cancel(); 116 | this.#ota = undefined; 117 | this.status({fill: "red", shape: "ring", text: e.toString()}); 118 | return; 119 | } 120 | 121 | this.received += count; 122 | if (!this.#ota) 123 | ; 124 | else if (undefined === this.total) 125 | this.status({fill: "green", shape: "dot", text: this.received + " bytes"}); 126 | else { 127 | const percent = Math.round((this.received / this.total) * 10000) / 100; 128 | this.status({fill: "green", shape: "dot", text: percent + "%"}); 129 | } 130 | } 131 | end(error, done) { 132 | let success = error ? false : true; 133 | 134 | if (!this.#ota) 135 | success = false; 136 | else if (error) { 137 | error = error.toString(); 138 | this.#ota.cancel(); 139 | this.status({fill: "red", shape: "ring", text: error.toString()}); 140 | done?.(error); 141 | } 142 | else { 143 | try { 144 | this.#ota.complete(); 145 | this.status({fill: "green", shape: "dot", text: "success"}); 146 | } 147 | catch (e) { 148 | error = e; 149 | this.status({fill: "red", shape: "ring", text: error.toString()}); 150 | success = false; 151 | } 152 | done?.(error); 153 | } 154 | this.#client?.close(); 155 | this.#client = this.#ota = undefined; 156 | 157 | return success; 158 | } 159 | 160 | static type = "ota-update"; 161 | static { 162 | RED.nodes.registerType(this.type, this); 163 | } 164 | } 165 | 166 | const route = Object.freeze({ 167 | onRequest(request) { 168 | this.node.begin(request.headers); 169 | }, 170 | onReadable(count) { 171 | this.node.append(this, count); 172 | }, 173 | onResponse(response) { 174 | const success = this.node.end(null); 175 | 176 | response.status = success ? 200 : 500; 177 | this.respond(response); 178 | 179 | if (success) 180 | this.node.send({payload: "ota push"}); 181 | }, 182 | onError(e) { 183 | this.node.end(e ?? new Error("OTA failed")); 184 | } 185 | }); 186 | -------------------------------------------------------------------------------- /nodes/ota-update/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@moddable-node-red/ota-update", 3 | "version": "0.8.8", 4 | "description": "Install OTA firmware updates in Node-RED MCU Edition", 5 | "scripts": { 6 | "test": "echo \"Error: no tests\" && exit 1" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/phoddie/node-red-mcu.git", 11 | "directory": "nodes/ota-update" 12 | }, 13 | "keywords": [ 14 | "mcu", 15 | "node-red", 16 | "node-red-mcu", 17 | "ota" 18 | ], 19 | "author": "Peter Hoddie", 20 | "license": "LGPL-3.0-or-later", 21 | "bugs": { 22 | "url": "https://github.com/phoddie/node-red-mcu/issues" 23 | }, 24 | "homepage": "https://github.com/phoddie/node-red-mcu/nodes/ota-update/readme.md", 25 | "engines": { 26 | "node": ">=14.0.0" 27 | }, 28 | "node-red": { 29 | "version": ">=3.0.0", 30 | "nodes": { 31 | "mcu_ota-update": "mcu_ota-update.js" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /nodes/ota-update/readme.md: -------------------------------------------------------------------------------- 1 | # OTA Update 2 | 3 | Install firmware updates. There are two modes of operation: 4 | 5 | - **Pull**. MCU downloads firmware update using HTTP GET. 6 | - **Push**. MCU receives firmware update through an HTTP PUT. 7 | 8 | This node runs on an MCU using [Node-RED MCU Edition](https://github.com/phoddie/node-red-mcu). 9 | 10 | ## Preparing an OTA Update 11 | The OTA Update node is currently implemented for MCUs in the ESP32 family. 12 | 13 | The OTA update file is an ESP-IDF binary firmware image file. The Moddable SDK build outputs this file as part of every build, as it is the file used to flash the firmware image during development. 14 | 15 | The path of the binary firmware image is based on the project name, platform/subplatform, and build type. For a release build of node-red-mcu for Moddable Two (`esp32/moddable_two`), the firmware image is at `$MODDABLE/build/bin/esp32/moddable_two/release/node-red-mcu/xs_esp32.bin`. 16 | 17 | When using the MCU plug-in to build, the project name is randomly generated, which would normally make it difficult to locate. Fortunately, the MCU plug-in log its working directory, which ends with the generated name (here it is `lyfr1bexk5`): 18 | 19 | ``` 20 | Working directory: /Users/username/.node-red/mcu-plugin-cache/lyfr1bexk5 21 | ``` 22 | 23 | ## Pull mode 24 | In Pull mode, the MCU downloads the firmware update using HTTP GET. 25 | 26 | ### Inputs 27 | 28 | : url (string) : URL of firmware image to download and install 29 | 30 | ### Outputs 31 | 32 | : payload (string) : On successful completion of OTA update, `"ota pull"`. 33 | 34 | ## Push mode 35 | In Push mode, the MCU receives the firmware update through an HTTP PUT. 36 | 37 | The `Push Endpoint` property in the Node-RED Editor is the HTTP endpoint to upload OTA updates to the MCU. Push mode is **disabled** by default. Set the upload path on the URL property to enable Push mode. 38 | 39 | ### Inputs 40 | 41 | None 42 | 43 | ### Outputs 44 | 45 | : payload (string) : On successful completion of OTA update, `"ota push"`. 46 | 47 | ## Restart after OTA 48 | The updated firmware begins running after the MCU is restarted. The OTA Update node does not automatically restart after a successful OTA install to allow flows to perform a graceful shutdown. Flows may use the [MCU restart node](https://flows.nodered.org/node/@moddable-node-red/mcu_restart) to reboot the microcontroller. 49 | 50 | ## Monitoring OTA progress 51 | When an OTA update has successfully been installed, `Complete` nodes are triggered. 52 | 53 | If an OTA update fails, `Error` nodes are triggered. 54 | 55 | `Status` nodes are triggered during the installation of an OTA update to report progress. 56 | 57 | ## Testing Push mode 58 | The `curl` command line tool may be used to push an OTA update to the microcontroller. Here's an example: 59 | 60 | ``` 61 | curl http://192.0.1.45/ota/firmware -X PUT -H "Content-Type: application/octet-stream" --data-binary '@xs_esp32.bin' 62 | ``` 63 | 64 | Note that this example assumes the current directory contains the firmware image in a file named `xs_esp32.bin`, that the microcontroller is reachable at IP address `192.0.1.45`, and that the OTA Node is configured with a Push Endpoint of `ota/firmware`. You will need to adjust these for your configuration. 65 | 66 | ## Push or Pull? 67 | There are good reasons to choose Push or Pull mode for OTA updates in your project. Some projects might even use both. Here are some considerations to help guide your choice. 68 | 69 | - Push mode is the simplest to integrate: add an OTA Update node, set the URL endpoint for receiving and upload, and connect the OTA Update node to an MCU Restart node. 70 | - Push mode is convenient for quickly deploying updates to an MCU. Just run `curl` to upload new firmware to the device. 71 | - Push mode opens the MCU for full firmware updates by anyone that can connect to its IP address. If that's a local network address and everyone on the network is trusted, that may be fine. If not, it may be a security risk. 72 | - Pull mode takes a bit more work to integrate into to a flow. In addition to the OTA Update and MCU Restart nodes, the flow needs a trigger to begin the OTA download. That trigger can come from any other Node-RED node in the flow. That might be an MQTT message received on an OTA topic, the push of a physical button on the device, the press of an on-screen button using the Node-RED Dashboard, or an HTTP POST with the URL of the OTA update. There's a lot of flexibility to decide how to trigger the OTA update. 73 | - Pull mode gives complete control over when the OTA is initiated, which allows the device to choose a non-disruptive time to apply the update. By contrast, the timing of Push mode updates is whenever the upload begins. 74 | - Pull mode works best for updating fleets of MCUs. A single message published to an MQTT OTA topic monitored by thousands of devices will update all of them. -------------------------------------------------------------------------------- /nodes/parser/csv/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "csv": "./70-CSV" 4 | }, 5 | "preload": [ 6 | "csv" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /nodes/restart/examples/example.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "585f999cf8ac5c84", 4 | "type": "tab", 5 | "label": "Flow 5", 6 | "disabled": false, 7 | "info": "", 8 | "env": [] 9 | }, 10 | { 11 | "id": "3bd56973f3fd25e4", 12 | "type": "inject", 13 | "z": "585f999cf8ac5c84", 14 | "name": "", 15 | "props": [ 16 | { 17 | "p": "payload" 18 | } 19 | ], 20 | "repeat": "", 21 | "crontab": "", 22 | "once": true, 23 | "onceDelay": "5", 24 | "topic": "", 25 | "payload": "", 26 | "payloadType": "date", 27 | "x": 270, 28 | "y": 380, 29 | "wires": [ 30 | [ 31 | "cb14341d99104da1" 32 | ] 33 | ] 34 | }, 35 | { 36 | "id": "cb14341d99104da1", 37 | "type": "mcu_restart", 38 | "z": "585f999cf8ac5c84", 39 | "name": "", 40 | "moddable_manifest": { 41 | "include": [ 42 | { 43 | "git": "https://github.com/phoddie/mcu_restart.git" 44 | } 45 | ] 46 | }, 47 | "x": 450, 48 | "y": 380, 49 | "wires": [] 50 | } 51 | ] -------------------------------------------------------------------------------- /nodes/restart/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "*": "./restart" 4 | }, 5 | "preload": [ 6 | "restart" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /nodes/restart/mcu_restart.html: -------------------------------------------------------------------------------- 1 | 22 | 23 | 29 | 30 | 59 | -------------------------------------------------------------------------------- /nodes/restart/mcu_restart.js: -------------------------------------------------------------------------------- 1 | module.exports = function(RED) { 2 | function RestartNode(config) { 3 | RED.nodes.createNode(this,config); 4 | 5 | this.on('input', (msg, send, done) => { 6 | done(); 7 | }); 8 | } 9 | RED.nodes.registerType("mcu_restart", RestartNode); 10 | } 11 | -------------------------------------------------------------------------------- /nodes/restart/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@moddable-node-red/mcu_restart", 3 | "version": "1.0.9", 4 | "description": "Restart microcontroller for Node-RED MCU Edition", 5 | "scripts": { 6 | "test": "echo \"Error: no tests\" && exit 1" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/phoddie/node-red-mcu.git", 11 | "directory": "nodes/restart" 12 | }, 13 | "keywords": [ 14 | "mcu", 15 | "node-red", 16 | "node-red-mcu", 17 | "restart" 18 | ], 19 | "author": "Peter Hoddie", 20 | "license": "LGPL-3.0-or-later", 21 | "bugs": { 22 | "url": "https://github.com/phoddie/node-red-mcu/issues" 23 | }, 24 | "homepage": "https://github.com/phoddie/node-red-mcu/nodes/restart/readme.md", 25 | "engines": { 26 | "node": ">=14.0.0" 27 | }, 28 | "node-red" : { 29 | "version": ">=3.0.0", 30 | "nodes": { 31 | "mcu_restart": "mcu_restart.js" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /nodes/restart/readme.md: -------------------------------------------------------------------------------- 1 | # Restart MCU 2 | 3 | Restarts the MCU. 4 | 5 | This node is designed for use with [Node-RED MCU Edition](https://github.com/phoddie/node-red-mcu). 6 | 7 | ### Inputs 8 | 9 | : payload (any) : payload is not used 10 | 11 | ### Outputs 12 | 13 | None 14 | 15 | ### Details 16 | 17 | The MCU is restarted when any message is received. This is useful in many situations: 18 | 19 | - Following the installation of an OTA update 20 | - After changing global settings 21 | - To restart long-running devices periodically 22 | - In response to unexpected errors 23 | 24 | When used in the Moddable SDK simulator (mcsim), the restart node exits the simulator. 25 | 26 | ### References 27 | 28 | - [GitHub](https://github.com/phoddie/mcu_restart) - this node's repository on GitHub 29 | - [Node-RED MCU Edition](https://github.com/phoddie/node-red-mcu) - the Node-RED MCU Edition repository 30 | -------------------------------------------------------------------------------- /nodes/restart/restart.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | 22 | #include "xs.h" 23 | #include "xsHost.h" 24 | 25 | void xs_mcu_restart(xsMachine *the) 26 | { 27 | #if ESP32 28 | esp_restart(); 29 | #elif defined(__ets__) 30 | system_restart(); 31 | esp_yield(); 32 | #elif defined(PICO_BUILD) 33 | pico_reset(); 34 | #else 35 | c_exit(1); 36 | #endif 37 | } 38 | -------------------------------------------------------------------------------- /nodes/restart/restart.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | import {Node} from "nodered"; 22 | 23 | class Restart extends Node { 24 | onMessage() @ "xs_mcu_restart" 25 | 26 | static type = "mcu_restart"; 27 | static { 28 | RED.nodes.registerType(this.type, this); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /nodes/rpi/rpi-ds18b20/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODULES)/drivers/onewire/manifest.json" 4 | ], 5 | "modules": { 6 | "*": [ 7 | "./rpi-ds18b20" 8 | ] 9 | }, 10 | "preload": [ 11 | "rpi-ds18b20" 12 | ], 13 | "platforms": { 14 | "esp": { 15 | "config": { 16 | "onewire": { 17 | "pin": 4 18 | } 19 | } 20 | }, 21 | "esp32": { 22 | "config": { 23 | "onewire": { 24 | "pin": 13 25 | } 26 | } 27 | }, 28 | "...": { 29 | "config": { 30 | "onewire": { 31 | "pin": "1" 32 | } 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /nodes/rpi/rpi-ds18b20/rpi-ds18b20.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | import {Node} from "nodered"; 22 | import mcconfig from "mc/config"; 23 | import OneWire from "onewire"; 24 | import DS18X20 from "DS18X20"; 25 | 26 | class RpiDS18B20 extends Node { 27 | #array; 28 | #bus; 29 | #sensors = []; 30 | 31 | onStart(config) { 32 | super.onStart(config); 33 | 34 | if (config.topic) 35 | throw new Error("topic unimplemented"); 36 | 37 | this.#array = config.array; 38 | 39 | this.#bus = new OneWire({ 40 | pin: mcconfig.onewire.pin 41 | }); 42 | 43 | const items = this.#bus.search(); 44 | for (let i = 0; i < items.length; i++) { 45 | const id = items[i]; 46 | this.#sensors.push(new DS18X20({bus: this.#bus, id})); 47 | } 48 | } 49 | onMessage(msg) { 50 | if (msg.array ?? this.#array) 51 | msg.payload = []; 52 | 53 | for (let i = 0, sensors = this.#sensors; i < sensors.length; i++) { 54 | sensors[i].getTemperature(temp => { 55 | const sample = { 56 | family: sensors[i].family.toString(16), 57 | id: sensors[i].toString(), 58 | temp 59 | } 60 | if (this.#array) { 61 | msg.payload.push(sample); 62 | if (msg.payload.length === sensors.length) 63 | this.send(msg); 64 | } 65 | else { 66 | msg.payload = sample; 67 | this.send(msg); 68 | } 69 | }); 70 | } 71 | } 72 | 73 | static type = "rpi-ds18b20"; 74 | static { 75 | RED.nodes.registerType(this.type, this); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /nodes/rpi/rpi-gpio/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "*": [ 4 | "./rpi-gpio" 5 | ] 6 | }, 7 | "preload": [ 8 | "rpi-gpio" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /nodes/rpi/rpi-gpio/rpi-gpio.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | import {Node} from "nodered"; 22 | import Timer from "timer"; 23 | 24 | let cache; // support multiple nodes sharing the same pin, like the RPi implementation 25 | 26 | class DigitalInNode extends Node { 27 | #debounce; 28 | #timer; 29 | 30 | onStart(config) { 31 | super.onStart(config); 32 | 33 | const Digital = globalThis.device?.io?.Digital; 34 | if (!Digital) 35 | return; 36 | 37 | this.#debounce = config.debounce; 38 | cache ??= new Map; 39 | let io = cache.get(config.pin); 40 | const intype = config.intype ?? "input" 41 | if (io) { 42 | if (io.type !== intype) 43 | return this.error("digital in mismatch") 44 | io.readers.push(this); 45 | } 46 | else { 47 | let mode = Digital.Input; 48 | if ("up" === intype) 49 | mode = Digital.InputPullUp; 50 | else if ("down" === intype) 51 | mode = Digital.InputPullDown; 52 | io = new Digital({ 53 | pin: config.pin, 54 | mode, 55 | edge: Digital.Rising + Digital.Falling, 56 | onReadable() { 57 | this.readers.forEach(reader => { 58 | reader.#timer ??= Timer.set(() => { 59 | reader.#timer = undefined; 60 | 61 | const msg = { 62 | payload: this.read(), 63 | topic: "gpio/" + this.pin 64 | } 65 | reader.send(msg) 66 | reader.status({fill: "green", shape: "dot", text: msg.payload}); 67 | }, reader.#debounce); 68 | }); 69 | } 70 | }); 71 | io.type = intype; 72 | io.pin = config.pin; 73 | io.readers = [this]; 74 | cache.set(config.pin, io); 75 | } 76 | 77 | if (config.read) { 78 | const payload = io.read(); 79 | this.send({ 80 | payload, 81 | topic: "gpio/" + config.pin 82 | }); 83 | this.status({fill: "green", shape: "dot", text: payload}); 84 | } 85 | } 86 | 87 | static type = "rpi-gpio in"; 88 | static { 89 | RED.nodes.registerType(this.type, this); 90 | } 91 | } 92 | 93 | class DigitalOutNode extends Node { 94 | #pin; 95 | #io; 96 | #hz; 97 | 98 | onStart(config) { 99 | super.onStart(config); 100 | 101 | this.#pin = config.pin; 102 | this.#hz = config.freq; 103 | 104 | const options = {pin: this.#pin}; 105 | 106 | cache ??= new Map; 107 | let io = cache.get(config.pin); 108 | if (undefined === this.#hz) { 109 | if (!globalThis.device?.io?.Digital) 110 | return; 111 | 112 | if (io) { 113 | if (io.type !== "output") 114 | return this.error("digital out mismatch") 115 | this.#io = io; 116 | } 117 | else { 118 | options.mode = device.io.Digital.Output; 119 | this.#io = io = new device.io.Digital(options); 120 | if (undefined !== config.level) 121 | io.write(config.level); 122 | io.type = "output"; 123 | cache.set(config.pin, io); 124 | } 125 | } 126 | else { 127 | if (!globalThis.device?.io?.PWM) 128 | return; 129 | 130 | if (io) { 131 | if (io.type !== "pwm") 132 | return this.error("digital out mismatch") 133 | this.#io = io; 134 | } 135 | else { 136 | if (this.#hz) 137 | options.hz = this.#hz; 138 | this.#io = io = new device.io.PWM(options); 139 | io.type = "pwm"; 140 | cache.set(config.pin, io); 141 | } 142 | } 143 | } 144 | onMessage(msg, done) { 145 | if (undefined === this.#hz) { 146 | this.#io?.write(msg.payload); 147 | trace(`digital out ${this.#pin}: ${msg.payload}\n`); 148 | } 149 | else { 150 | const value = (parseFloat(msg.payload) / 100) * ((1 << (this.#io?.resolution ?? 8)) - 1); 151 | this.#io?.write(value); 152 | trace(`PWM ${this.#pin}: ${value}\n`); 153 | } 154 | this.status({fill:"green", shape:"dot", text: msg.payload.toString()}); 155 | done(); 156 | } 157 | 158 | static type = "rpi-gpio out"; 159 | static { 160 | RED.nodes.registerType(this.type, this); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /nodes/rpi/rpi-neopixels/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "*": [ 4 | "../../mcu/neopixels/colors" 5 | ] 6 | }, 7 | "preload": [ 8 | "colors" 9 | ], 10 | "platforms": { 11 | "esp32": { 12 | "include": "$(MODULES)/drivers/neopixel/manifest.json", 13 | "modules": { 14 | "*": "./rpi-neopixels" 15 | }, 16 | "preload": "rpi-neopixels" 17 | }, 18 | "pico": { 19 | "include": "$(MODULES)/drivers/neopixel/manifest.json", 20 | "modules": { 21 | "*": "./rpi-neopixels" 22 | }, 23 | "preload": "rpi-neopixels" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /nodes/sequence/batch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "batch": "./19-batch" 4 | }, 5 | "preload": [ 6 | "batch" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /nodes/sequence/join/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "join": "./17-join" 4 | }, 5 | "preload": [ 6 | "join" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /nodes/sequence/sort/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "sort": "./18-sort" 4 | }, 5 | "preload": [ 6 | "sort" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /nodes/sequence/split/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "split": "./split" 4 | }, 5 | "preload": [ 6 | "split" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /nodes/sequence/split/split.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | import {Node} from "nodered"; 22 | 23 | class SplitNode extends Node { 24 | #arraySplt; 25 | #splt; 26 | #spltType; 27 | #addname; 28 | #stream; 29 | 30 | onStart(config) { 31 | super.onStart(config); 32 | 33 | this.#spltType = config.spltType; 34 | this.#splt = config.splt; 35 | this.#arraySplt = config.arraySplt; 36 | this.#addname = config.addname; 37 | if (config.stream) 38 | this.#stream = {}; 39 | } 40 | onMessage(msg, done) { 41 | let payload = msg.payload; 42 | let stream = this.#stream; 43 | 44 | if (payload instanceof Uint8Array) { 45 | let splt = this.#splt; 46 | if ("str" === this.#spltType) 47 | splt = new Uint8Array(ArrayBuffer.fromString(splt)); 48 | 49 | if (stream) { 50 | stream.buf ??= {dones: [], index: 0}; 51 | stream = stream.buf; 52 | if (stream.remainder) 53 | payload = Buffer.concat([stream.remainder, payload]); 54 | 55 | const length = payload.length; 56 | if ("len" === this.#spltType) { 57 | if (length < splt) { 58 | stream.remainder = payload; 59 | stream.dones.push(done); 60 | return; 61 | } 62 | const i = length % splt; 63 | if (i) { 64 | stream.remainder = Uint8Array.prototype.slice.call(payload, length - i); 65 | payload = payload.subarray(0, length - i); 66 | } 67 | else 68 | delete stream.remainder; 69 | } 70 | else { 71 | const i = Buffer.prototype.lastIndexOf.call(payload, splt); 72 | if (i < 0) { 73 | stream.remainder = payload; 74 | stream.dones.push(done); 75 | return; 76 | } 77 | if ((i + splt.length) !== length) { 78 | stream.remainder = Uint8Array.prototype.slice.call(payload, i + splt.length); 79 | payload = payload.subarray(0, i); 80 | } 81 | else { 82 | delete stream.remainder; 83 | payload = payload.subarray(0, i); 84 | } 85 | } 86 | } 87 | 88 | const parts = {type: "buffer", id: RED.util.generateId()}; 89 | msg.parts = parts; 90 | 91 | if (stream) 92 | parts.index = stream.index; 93 | else 94 | parts.index = 0; 95 | 96 | if ("len" === this.#spltType) { 97 | const length = payload.length; 98 | parts.ch = splt; 99 | if (!stream) 100 | parts.count = Math.idiv((length + splt - 1), splt); 101 | for (let i = 0; i < length; i += splt) { 102 | msg.payload = Uint8Array.prototype.slice.call(payload, i, i + splt); 103 | parts.index += 1; 104 | this.send(msg); 105 | } 106 | } 107 | else if (("bin" === this.#spltType) || ("str" === this.#spltType)) { 108 | const b = payload; 109 | parts.ch = splt; 110 | payload = []; 111 | let position = 0; 112 | while (position < b.length) { 113 | let next = Buffer.prototype.indexOf.call(b, splt, position); 114 | if (next < 0) 115 | next = b.length; 116 | payload.push(Uint8Array.prototype.slice.call(b, position, next)); 117 | position = next + splt.byteLength; 118 | } 119 | let length = payload.length; 120 | if (!stream) 121 | parts.count = length; 122 | for (let i = 0; i < length; i += 1) { 123 | msg.payload = payload[i] 124 | parts.index += 1; 125 | this.send(msg); 126 | } 127 | } 128 | if (stream) 129 | stream.index = parts.index; 130 | } 131 | else if (Array.isArray(payload)) { 132 | const length = payload.length, arraySplt = this.#arraySplt; 133 | const parts = {type: "array", count: Math.idiv(length + arraySplt - 1, arraySplt), len: arraySplt, id: RED.util.generateId()}; 134 | msg.parts = parts; 135 | for (let i = 0; i < length; i += arraySplt) { 136 | msg.payload = (1 === arraySplt) ? payload[i] : payload.slice(i, i + arraySplt); 137 | parts.index = Math.idiv(i, arraySplt); 138 | this.send(msg); 139 | } 140 | } 141 | else if ("object" === typeof payload) { 142 | const names = Object.getOwnPropertyNames(payload); 143 | const length = names.length; 144 | const parts = {type: "object", count: length, id: RED.util.generateId()}; 145 | msg.parts = parts; 146 | for (let i = 0, addname = this.#addname; i < length; i += 1) { 147 | const key = names[i]; 148 | msg.payload = payload[key]; 149 | parts.index = i; 150 | parts.key = key; 151 | if (addname) 152 | msg[addname] = key; 153 | this.send(msg); 154 | } 155 | } 156 | else { // string 157 | 158 | payload = payload.toString(); 159 | let splt = this.#splt; 160 | if ("bin" === this.#spltType) 161 | splt = String.fromArrayBuffer(splt) 162 | 163 | if (stream) { 164 | stream.str ??= {dones: [], index: 0}; 165 | stream = stream.str; 166 | if (stream.remainder) 167 | payload = stream.remainder + payload; 168 | 169 | const length = payload.length; 170 | if ("len" === this.#spltType) { 171 | if (length < splt) { 172 | stream.remainder = payload; 173 | stream.dones.push(done); 174 | return; 175 | } 176 | const i = length % splt; 177 | if (i) { 178 | stream.remainder = payload.slice(length - i); 179 | payload = payload.slice(0, length - i); 180 | } 181 | else 182 | delete stream.remainder; 183 | } 184 | else { 185 | const i = payload.lastIndexOf(splt); 186 | if (i < 0) { 187 | stream.remainder = payload; 188 | stream.dones.push(done); 189 | return; 190 | } 191 | if ((i + splt.length) !== length) { 192 | stream.remainder = payload.slice(i + splt.length); 193 | payload = payload.slice(0, i); 194 | } 195 | else { 196 | delete stream.remainder; 197 | payload = payload.slice(0, i); 198 | } 199 | } 200 | } 201 | 202 | if ("len" === this.#spltType) { 203 | const s = payload, length = s.length; 204 | payload = new Array(Math.idiv(length + splt - 1, splt)); 205 | payload.fill(undefined); 206 | for (let i = 0, j = 0; i < length; i += splt, j += 1) 207 | payload[j] = s.slice(i, i + splt); 208 | } 209 | else 210 | payload = payload.split(splt); 211 | 212 | const length = payload.length; 213 | const parts = {type: "string", ch: splt, id: RED.util.generateId()}; 214 | if (stream) { 215 | parts.index = stream.index; 216 | stream.index = parts.index + length; 217 | } 218 | else { 219 | parts.count = length; 220 | parts.index = 0; 221 | } 222 | msg.parts = parts; 223 | for (let i = 0; i < length; i += 1) { 224 | msg.payload = payload[i]; 225 | this.send(msg); 226 | parts.index += 1; 227 | } 228 | } 229 | 230 | if (stream) { 231 | stream.dones.forEach(done => done()); 232 | if (stream.remainder) { 233 | stream.dones.length = 1; 234 | stream.dones[0] = done; 235 | } 236 | else { 237 | stream.dones.length = 0; 238 | done(); 239 | } 240 | } 241 | else 242 | done(); 243 | } 244 | 245 | static type = "split"; 246 | static { 247 | RED.nodes.registerType(this.type, this); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /nodes/storage/file/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/modules/files/file/manifest_littlefs.json", 4 | "$(MODDABLE)/modules/data/base64/manifest.json", 5 | "$(MODDABLE)/modules/data/hex/manifest.json" 6 | ], 7 | "modules": { 8 | "*": "./node-red-files" 9 | }, 10 | "preload": "node-red-files" 11 | } 12 | -------------------------------------------------------------------------------- /nodes/weather/openweathermap/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/io/tcp/fetch/manifest_fetch.json" 4 | ], 5 | "modules": { 6 | "openweathermap": "./weather" 7 | }, 8 | "preload": "openweathermap" 9 | } 10 | -------------------------------------------------------------------------------- /setuptimezone.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | import "nodered"; // import for global side effects 22 | import Time from "time"; 23 | import Preference from "preference"; 24 | 25 | export default function (done) { 26 | const timezone = Preference.get("time", "zone"); 27 | const dst = Preference.get("time", "dst"); 28 | if (RED.mcu.debugging()) { 29 | if (Time.timezone !== timezone) 30 | Preference.set("time", "zone", Time.timezone); 31 | if (Time.dst !== dst) 32 | Preference.set("time", "dst", Time.dst); 33 | } 34 | else if ((undefined !== timezone) && (undefined !== dst)) { 35 | Time.timezone = timezone; 36 | Time.dst = dst; 37 | } 38 | 39 | done(); 40 | } 41 | -------------------------------------------------------------------------------- /setupwifi.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2023 Moddable Tech, Inc. 3 | * 4 | * This file is part of the Moddable SDK Runtime. 5 | * 6 | * The Moddable SDK Runtime is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * The Moddable SDK Runtime is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with the Moddable SDK Runtime. If not, see . 18 | * 19 | */ 20 | 21 | import config from "mc/config"; 22 | import Time from "time"; 23 | import WiFi from "wifi/connection"; 24 | import Net from "net"; 25 | import SNTP from "sntp"; 26 | import Modules from "modules"; 27 | 28 | export default function (done) { 29 | const modconfig = Modules.has("mod/config") ? Modules.importNow("mod/config") : {}; 30 | 31 | WiFi.mode = 1; 32 | 33 | const ssid = modconfig.ssid ?? config.ssid; 34 | const password = modconfig.password ?? config.password; 35 | const sntp = modconfig.sntp ?? config.sntp; 36 | if (!ssid) { 37 | trace("No Wi-Fi SSID\n"); 38 | return done(); 39 | } 40 | 41 | new WiFi({ssid, password}, function(msg, code) { 42 | switch (msg) { 43 | case WiFi.gotIP: 44 | trace(`IP address ${Net.get("IP")}\n`); 45 | 46 | if (!sntp || (Date.now() > 1672722071_000)) { 47 | done?.(); 48 | done = undefined; 49 | return; 50 | } 51 | 52 | new SNTP({host: sntp}, function(message, value) { 53 | if (SNTP.time === message) { 54 | trace("got time\n"); 55 | Time.set(value); 56 | } 57 | else if (SNTP.error === message) 58 | trace("can't get time\n"); 59 | else 60 | return; 61 | done?.(); 62 | done = undefined; 63 | }); 64 | break; 65 | 66 | case WiFi.connected: 67 | trace(`Wi-Fi connected to "${Net.get("SSID")}"\n`); 68 | break; 69 | 70 | case WiFi.disconnected: 71 | trace((-1 === code) ? "Wi-Fi password rejected\n" : "Wi-Fi disconnected\n"); 72 | break; 73 | } 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /tools/xsbugproxy/index.js: -------------------------------------------------------------------------------- 1 | const net = require('node:net'); 2 | 3 | const proxyPortIn = 5004; // proxy listening port 4 | 5 | /* 6 | Moddable SDK projects connect to xsbug at loaclhost:5002. 7 | This proxy listens on loaclhost:5004. To have the Noddable SDK project to connect to the proxy 8 | requires a manual source code change. On macOS, in $MODDABLE/xs/platforms/mac_xs.c, change 9 | address.sin_port = htons(5002); 10 | to 11 | address.sin_port = htons(5004); 12 | Linux and Windows have similar changes. Eventually We can add an option to mcconfig to set the xsbug 13 | port. 14 | 15 | To work with an MCU connected via serial, change $MODDABLE/tools/serial2xsbug/serial2xsbug.c from: 16 | self->port = 5002; 17 | to 18 | self->port = 5004; 19 | Then rebuild the Moddable SDK tools: 20 | cd $MODDABLE/build/makefiles/mac 21 | make 22 | */ 23 | 24 | const proxyPortOut = 5002; // xsbug listening port 25 | const trace = false; // trace progress to console for debugging 26 | const relay = true; 27 | /* 28 | When relay is true, proxy relays messages between Moddable SDK project and xsbug. 29 | This allows using xsbug as usual while proxy has access to all communication. 30 | When relay is false, no cconnection is made to xsbug. The Moddabe SDK project sends 31 | messages as-if xsbug is present. 32 | */ 33 | 34 | const server = net.createServer(target => { 35 | if (trace) 36 | console.log('target connected'); 37 | 38 | let xsbug; 39 | if (relay) { 40 | // connect to xsbug to be able to relay messages 41 | xsbug = net.connect({ 42 | port: proxyPortOut, 43 | host: "localhost" 44 | }); 45 | xsbug.setEncoding("utf8"); 46 | xsbug.on('ready', data => { 47 | while (xsbug.deferred.length) 48 | xsbug.write(xsbug.deferred.shift()); 49 | delete xsbug.deferred; 50 | }); 51 | xsbug.on('data', data => { 52 | if (trace) 53 | console.log("from xsbug: " + data); 54 | target.write(data); 55 | }); 56 | xsbug.on('end', () => { 57 | if (trace) 58 | console.log('xsbug disconnected'); 59 | target.destroy(); 60 | }); 61 | xsbug.deferred = []; 62 | xsbug.deferred.push("2"); 63 | } 64 | else { 65 | } 66 | 67 | target.setEncoding("utf8"); 68 | let first = true; 69 | let timeout; 70 | target.on('data', data => { 71 | if (trace) 72 | console.log("to xsbug: " + data); 73 | 74 | // parse messages here 75 | // each message is an XML document 76 | // status messages are sent in a bubble right message of the form: 77 | // JSON STATUS MESSAGE HERE 78 | 79 | // example of sending a command to the Node-RED MCU runtime. 80 | // sends an "inject" ccommand the node in the specified flow 81 | if (undefined === timeout) { 82 | timeout = setTimeout(() => { 83 | const options = { 84 | command: "inject", 85 | flow: "d908d28ca7c5c7b4", 86 | id: "cfef9cb598d205ef" 87 | }; 88 | target.write(`\r\n\r\n`); 89 | }, 1000); 90 | } 91 | 92 | if (relay) { 93 | if (xsbug.deferred) 94 | xsbug.deferred.push(data); 95 | else 96 | xsbug.write(data); 97 | } 98 | else { 99 | if (first) { 100 | // first time need to send set-all-breakpoints as xsbug does 101 | first = false;; 102 | target.write('\r\n\r\n'); 103 | } 104 | else { 105 | // assume any other messages are a break, so send go. This isn't always corrrect but may always work. 106 | target.write('\r\n\r\n'); 107 | } 108 | } 109 | }); 110 | target.on('end', () => { 111 | if (trace) 112 | console.log('target disconnected'); 113 | if (xsbug) 114 | xsbug.destroy(); 115 | }); 116 | }); 117 | 118 | server.listen(proxyPortIn, () => { 119 | console.log('proxy listening'); 120 | }); 121 | -------------------------------------------------------------------------------- /tools/xsbugproxy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "ws": "^8.8.0" 4 | } 5 | } 6 | --------------------------------------------------------------------------------