├── 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 |
--------------------------------------------------------------------------------