├── .github └── workflows │ └── npm-publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── https.md ├── lib ├── library.js ├── manifest.js ├── proxy.js └── relay.js ├── mcu_modules ├── eta │ └── manifest.json ├── fs │ ├── fs.js │ └── manifest.json ├── node-red-contrib-semaphore │ └── manifest.json ├── node-red-node-serialport │ ├── README.md │ ├── manifest.json │ ├── portpath.png │ └── serialport.js ├── path │ ├── manifest.json │ └── path.js ├── require │ ├── manifest.json │ └── require.js └── semaphore │ ├── manifest.json │ └── semaphore.js ├── mcu_plugin.html ├── mcu_plugin.js ├── mods.md ├── package.json ├── package.md ├── pullmcu.js ├── resources ├── build_host.png ├── build_mode_button.png ├── junctiontest.png ├── lib │ ├── espressif.js │ ├── flashTab.js │ ├── xsbug.js │ └── xsserial.js ├── mcu_example.png └── mcu_panel.png ├── templates ├── main.js.eta └── main_mod_host_ui_js.eta └── test └── junction_resolver_test.json /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: "NPM Publish" 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 20 18 | # - run: npm ci 19 | # - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: actions/setup-node@v4 27 | with: 28 | node-version: 20 29 | registry-url: https://registry.npmjs.org/ 30 | # - run: npm ci 31 | - run: npm publish --access public 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.npm_access_token}} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # node-red-mcu-plugin 107 | package-lock.json 108 | node-red-mcu/ 109 | 110 | mcu-orig.js 111 | mcu-orig.html 112 | 113 | archive/ 114 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ralph Wetzel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-red-mcu-plugin 2 | Plugin to integrate [Node-RED MCU Edition](https://github.com/phoddie/node-red-mcu) into the Node-RED editor. 3 | 4 | ## Overview 5 | Node-RED MCU Edition is an implementation of the Node-RED runtime that runs on resource-constrained microcontrollers (MCUs). 6 | 7 | This plugin supports the process to build flows with & for Node-RED MCU Edition and interfaces between the Node-RED Editor and a connected MCU. 8 | 9 | It adds a side panel labeled "MCU" into the Node-RED Editor: 10 | 11 | mcu_panel 13 | 14 | The top section of this side panel allows to select the flows that shall be build for the MCU environment. 15 | Please be aware that **you have to deploy the selected flows** after you've made your choice. 16 | 17 | > Please be aware, that flows dedicated to MCU are in stand-by mode, awaiting an incoming MCU connection. 18 | > De-select them & deploy again to enable standard Node-RED functionality. 19 | 20 | In the bottom section of the side panel, several configurations defining compiler options may be prepared. This allows e.g. to select the target platform or the port used to address a dedicated hardware device. For option reference, see the `mcconfig` [documentation](https://github.com/Moddable-OpenSource/moddable/blob/public/documentation/tools/tools.md#arguments) of the Moddable SDK. 21 | 22 | Building the selected flows is as simple as triggering the `Build` button of one of the defined build configurations. 23 | 24 | You may follow the build process on the tab `Console Monitor`. 25 | 26 | ## Implemented Functionality 27 | 28 | - [x] Select flows to build. 29 | - [x] UI to define build targets & parameters. 30 | - [x] Console monitor - to follow the build process. 31 | - [x] Display status message of a node (running @ MCU) in the editor. 32 | - [x] Forward user trigger (e.g. `inject` node) to MCU. 33 | - [x] Debug node (from MCU back into the editor). 34 | - [x] Create `manifest.json` files for (any kind of) nodes / npm packages. 35 | - [x] `manifest.json` [library](#manifestjson) - providing pre-defined build parameters for (node modules and) npm packages 36 | - [x] Include relevant config nodes in the MCU build. 37 | - [x] Junction node resolving. 38 | - [x] Link node resolving. 39 | - [x] Support for ui_nodes. 40 | - [x] Build flows on macOS. 41 | - [x] Build flows on Windows. 42 | - [x] Build flows on Linux. 43 | - [x] Build flows when running Node-RED as service on Raspberry Pi. 44 | 45 | 46 | ## Test Case 47 | Although it looks minimalistic, the following flow shows most of the funcitonality in action: 48 | 49 | mcu_example 51 | 52 |
53 | flow.json 54 | 55 | ``` json 56 | [ 57 | { 58 | "id": "18785fa6f5606659", 59 | "type": "tab", 60 | "label": "MCU Tester", 61 | "disabled": false, 62 | "info": "", 63 | "env": [], 64 | "_mcu": true 65 | }, 66 | { 67 | "id": "1eeaa9a8a8c6f9f8", 68 | "type": "inject", 69 | "z": "18785fa6f5606659", 70 | "name": "", 71 | "props": [ 72 | { 73 | "p": "payload" 74 | } 75 | ], 76 | "repeat": "", 77 | "crontab": "", 78 | "once": false, 79 | "onceDelay": "3", 80 | "topic": "", 81 | "payload": "TEST", 82 | "payloadType": "str", 83 | "_mcu": true, 84 | "x": 330, 85 | "y": 200, 86 | "wires": [ 87 | [ 88 | "8f505d28fa1fb6e0" 89 | ] 90 | ] 91 | }, 92 | { 93 | "id": "da00311ba5215864", 94 | "type": "debug", 95 | "z": "18785fa6f5606659", 96 | "name": "Debug from MCU", 97 | "active": true, 98 | "tosidebar": true, 99 | "console": false, 100 | "tostatus": true, 101 | "complete": "true", 102 | "targetType": "full", 103 | "statusVal": "payload", 104 | "statusType": "auto", 105 | "_mcu": true, 106 | "x": 690, 107 | "y": 200, 108 | "wires": [] 109 | }, 110 | { 111 | "id": "8f505d28fa1fb6e0", 112 | "type": "lower-case", 113 | "z": "18785fa6f5606659", 114 | "name": "", 115 | "_mcu": true, 116 | "x": 490, 117 | "y": 200, 118 | "wires": [ 119 | [ 120 | "da00311ba5215864" 121 | ] 122 | ] 123 | } 124 | ] 125 | ``` 126 |
127 | 128 | ## Prerequisites 129 | 1) [node-red](https://www.nodered.org) 130 | 2) [Moddable SDK](https://github.com/Moddable-OpenSource/moddable) 131 | 132 | ## Installation 133 | 134 | ``` bash 135 | cd 136 | ``` 137 | then 138 | ```bash 139 | npm install @ralphwetzel/node-red-mcu-plugin 140 | ``` 141 | 142 | > This installs as well [node-red-mcu](https://github.com/phoddie/node-red-mcu). 143 | 144 | Please refer to the [Node-RED documentation](https://nodered.org/docs/user-guide/runtime/configuration) for details regarding ``. 145 | 146 | ### Raspberry Pi: Additional preparation steps 147 | 148 | #### Define MODDABLE 149 | The Node-RED documentation states a [slick comment]((https://nodered.org/docs/user-guide/environment-variables#running-as-a-service)) that things are a bit different when Node-RED is run as a service: 150 | > When Node-RED is running as a service having been installed using the provided [script](https://nodered.org/docs/getting-started/raspberrypi), it will not have access to environment variables that are defined only in the calling process. 151 | 152 | The _solution_ is stated as well: 153 | 154 | > In this instance, environment variables can be defined in the settings file by adding `process.env.FOO='World';` 155 | placed outside the module.exports section. Alternatively, variables can be defined as part of the systemd service by placing statements of the form `ENV_VAR='foobar'` in a file named environment within the Node-RED user directory, `~/.node-red`. 156 | 157 | Thus, please add `MODDABLE` as environment variable to your `settings.js`. 158 | 159 | ``` javascript 160 | module.exports = { 161 | [...] 162 | } 163 | 164 | // Please add MODDABLE *outside* the module.exports definition! 165 | process.env.MODDABLE = "/home/pi/Projects/moddable" 166 | ``` 167 | 168 | Make sure to provide the absolute path to the `MODDABLE` directory. If you're unsure, just run the following command in a shell to get the current definition: 169 | 170 | ``` bash 171 | pi@somebox:/ $echo $MODDABLE 172 | /home/pi/Projects/moddable 173 | pi@somebox:/ $ 174 | 175 | ``` 176 | 177 | #### Update your IDF toolchain 178 | There's a [significant issue in IDFv4.4](https://github.com/espressif/esp-idf/issues/7857) that lets the build process error out in certain situations with a dramatic comment: 179 | 180 | > gcc is not able to compile a simple test program. 181 | 182 | Whereas the issue documentation does not provide a solid fix for this situation, you **might be** able to overcome it by updating your toolchain - to the latest `release/v4.4` branch. 183 | 184 | ``` bash 185 | cd ~/esp32/esp-idf 186 | git checkout release/v4.4 187 | git submodule update 188 | ./install.sh 189 | . export.sh 190 | ``` 191 | 192 | Please be advised that updating the toolchain in that way could have sideeffects that cannot be predicted & might lead you into additional trouble! Thus: take care! 193 | 194 | ## Technical Details 195 | 196 | ### Build Environment 197 | This plugin creates the build environment in 198 | ``` 199 | /mcu-plugin-cache 200 | ``` 201 | Please refer to the [Node-RED documentation](https://nodered.org/docs/user-guide/runtime/configuration) for details regarding ``. 202 | 203 | There's a dedicated folder for each of the build configurations you have defined in the Node-RED editor. 204 | This folder - currently - is being emptied prio to each build run. 205 | 206 | ### Junction node resolving 207 | Junction nodes are a brilliant feature of the Node-RED editor to support the creation of cleary structured flows. 208 | In essence, they yet are just visual sugar to please the operator's eye. In runtime conditions, they still demand resources like any other node does. 209 | As we consider resources as always rare - which is especially true for any MCU - this plugin thus replaces all junction nodes by direct connections between two active nodes. It as well removes circular references in the junction node chain - if they exists. 210 | 211 | To test this feature, you may start with the [displayed flow](https://github.com/ralphwetzel/node-red-mcu-plugin/tree/main/test/junction_resolver_test.json): 212 | 213 | junction_resolver_test_flow 215 | 216 | 217 | ### manifest.json 218 | The [documentation of Moddable SDK](https://github.com/Moddable-OpenSource/moddable/blob/public/documentation/tools/manifest.md#manifest) states that 219 | 220 | > [a] manifest is a JSON file that describes the modules and resources necessary to build a Moddable app. 221 | 222 | One major task of this plugin is therefore the creation of the necessary `manifest.json` file(s) to enable the process of building the flows for the MCU. 223 | 224 | As all Node-RED nodes are organized as `npm` packages, this plugin extracts the necessary information from the dedicated `package.json` file(s) to process it into a manifest. Dependencies are resolved & additional manifests created as demanded. 225 | 226 | The manifests are organized in a structure mirroring the `node_modules` directory. 227 | 228 | There are two issue though: 229 | - Dependencies, that are not listed in `package.json` cannot be resolved. This is especially relevant in cases when a node `require`s one of the `Node.js` core libraries. 230 | - The `manifest.json` auto creation process (currently & definitely for some time) has it's limitations. 231 | 232 | To compensate for these issues, this plugin provides a (currently very small, potentially growing) manifest library for dedicated `npm` packages and `Node.js` modules. This allows to provide fine-tuned manifests that are guaranteed to be exhaustive; in the process to setup manifests those from the library have preference versus the generated ones. 233 | 234 | > **This manifest library calls for your contribution!** Feel free to provide manifests for the nodes & packages you're working with. Each manifest added improves the performance of the node-red-mcu eco system! 235 | 236 | Let me give you a **WARNING:** 237 | I'm pretty sure that **every** (non-standard) node you try to build for MCU will currently demand some additional efforts. Please raise an issue if you need support setting up the `manifest.json` accordingly. 238 | 239 | -------------------------------------------------------------------------------- /https.md: -------------------------------------------------------------------------------- 1 | # HTTPS support for your local server running Node-RED 2 | 3 | To use some of the advanced features of this plugin, the server running Node-RED needs to support the `https` protocol, even as it's located in your local network only. Usually you yet don't want to invest in a commercial certificate, just to get these credentials for your little Pi. As it shows up, this as well isn't necessary at all: 4 | 5 | The following procedure illustrates a way to create first your own locally trusted Certification Authority to then issue the right credentials for your local Node-RED server. 6 | 7 | We anticipate, that you're working currently with your development system (devSys), that might be a Laptop, MacBook, Desktop computer or whatever. Somewhere in your local network, there's the system, e.g. a Pi, running your Node-RED server; for the sake of simplicity, we'll call this system from now on "your Pi", even if it was something else. 8 | 9 | Please be aware that you have to have `root` / `admin` privileges on both systems to finish this procedure successfully! 10 | 11 | Let's begin! 12 | 13 | ## Create your own local CA (Certifcation Authority) 14 | Download and install [`mkcert`](https://github.com/FiloSottile/mkcert) on your devSys. 15 | 16 | Run `mkcert -install` to create and install the CA. This will popup (in case several times) a dialog window to confirm the change by entering an `admin` password. 17 | 18 | ## Check your trust store 19 | You should check your trust store (on Mac this is called Keychain) then to verify that our local CA certificate was installed successful. Finding the certificate in the trust store (most probably under `Certificates`) gives you as well a hint relevant for later: The domain name your devSys considers itself being part of. In case your local network is controlled by a FritzBox, this most probably is "fritz.box"; another options may be e.g. "local". Write this down, you'll need it. 20 | 21 | ## Identify the hostname of your Pi 22 | As a prerequisite to generate the correct certificate, you need to know the hostname of your Pi. Access it, then run 23 | 24 | ``` bash 25 | hostname 26 | ``` 27 | 28 | in a Terminal. Keep in mind, that each of your devices should have a different hostname. In case, you may change it now... 29 | 30 | ## Generate the credentials for your Pi 31 | On your sysDev, run 32 | ``` bash 33 | mkcert -client . 34 | ``` 35 | 36 | Use the hostname you identified in the step before as ``, the domain name from the trust store as ``. 37 | 38 | `mkcert` will now create two files: 39 | 40 | ``` 41 | .-client.pem 42 | .-client-key.pem 43 | ``` 44 | 45 | and tell you, where those have been saved in your directory tree. 46 | 47 | ## Install the credentials for the Node-RED server on your Pi 48 | 49 | Copy the two files generated in the step before to your Pi. 50 | That done, follow the description given in the Node-RED documentation to [secure Node-RED](https://nodered.org/docs/user-guide/runtime/securing-node-red). 51 | 52 | The `...-key.pem` file holds the required `key`, the other one the `cert`. 53 | 54 | I propose you use absolute path definitions when including the file data into `settings.js`. That could save you some trouble... 55 | 56 | ## Restart your Node-RED server 57 | Run 58 | 59 | ``` 60 | node-red-stop 61 | node-red-start 62 | ``` 63 | 64 | to restart the Node-RED server. 65 | 66 | ## Access Node-RED via https 67 | 68 | On your devSys, open a browser window & enter 69 | 70 | ``` 71 | https://.:1880 72 | ``` 73 | 74 | That should open Node-RED, indicating a nice looking lock symbol in the address bar - indicating a trusted & secure connection. 75 | 76 | Job done! -------------------------------------------------------------------------------- /lib/library.js: -------------------------------------------------------------------------------- 1 | const resolvePackagePath = require('resolve-package-path'); 2 | const path = require("path"); 3 | 4 | 5 | class nodes_library { 6 | constructor() { 7 | this.nodes = {} 8 | } 9 | 10 | /* Examine node definition and extract type & module information 11 | * 12 | * @param node: node data object as generated by Node-REDs loadNodeConfig 13 | * (node_modules/@node-red/registry/lib/loader.js) 14 | */ 15 | register_node(node) { 16 | 17 | /* @ 20220812: 18 | var node = { 19 | type: "node", 20 | id: id, 21 | module: module, 22 | name: name, 23 | file: file, 24 | template: file.replace(/\.js$/,".html"), 25 | enabled: isEnabled, 26 | loaded:false, 27 | version: version, 28 | local: fileInfo.local, 29 | types: [], 30 | config: "", 31 | help: {} 32 | }; 33 | */ 34 | 35 | if (node.type !== "node") return; 36 | if (!node.types || node.types.length < 1) return; 37 | if (!node.module || !node.file) return; 38 | 39 | let pp = resolvePackagePath(node.module, node.file); 40 | if (!pp) return; 41 | pp = path.parse(pp).dir; 42 | 43 | for (let i=0; i 0) { 107 | // we wish to have the manifest.json in the /mcu subdirectory 108 | paths_to_check.push(path.join(_path, "mcu")); 109 | // ... but accept it as well in the root 110 | paths_to_check.push(_path); 111 | } 112 | } 113 | } 114 | 115 | } 116 | 117 | // A very convenient situation: there is a manifest for this node type! 118 | paths_to_check.push(path.join(module_path, "mcu")) 119 | 120 | // This is deprecated: accept as well a "manifest.json" in the nodes root directory 121 | paths_to_check.push(module_path); 122 | 123 | // Next best: We've a manifest template provided predefined in our mcu_modules folder 124 | let scoped_module = module.split("/"); 125 | paths_to_check.push(path.join(this.mcu_modules_path, ...scoped_module)); 126 | 127 | // Perhaps there's already a manifest.json in the (optionally) provided path 128 | if (optional_path) { 129 | paths_to_check.push(path.join(optional_path, ...scoped_module)) 130 | } 131 | 132 | for (let i=0; i "@node-red/nodes" 382 | for (let i=0; i 0) { 400 | _path = _path.slice(0, -_pp.ext.length); 401 | } 402 | bldr.add_module(_path, _module); 403 | 404 | if (this.options?.preload === true) { 405 | bldr.add_preload(_module); 406 | } 407 | } 408 | 409 | // Write this initial manifest to disc 410 | // to ensure that it's found on further iterations 411 | // thus to stop the iteration! 412 | // console.log(mnfst_path); 413 | // console.log(bldr.get()); 414 | 415 | fs.writeFileSync(mnfst_path, bldr.get(), (err) => { 416 | if (err) { 417 | throw err; 418 | } 419 | }); 420 | 421 | let changed = false; 422 | 423 | // console.log(`Checking dependencies of module "${module}":`); 424 | 425 | let deps = pckge.dependencies; 426 | if (deps) { 427 | for (let key in deps) { 428 | if (check_template("include", key)) { 429 | let mnfst = this.get_manifest_of_module(key, destination); 430 | if (mnfst && typeof (mnfst) === "string") { 431 | bldr.include_manifest(mnfst); 432 | changed = true; 433 | continue; 434 | } 435 | mnfst = this.create_manifests_for_module(key, destination); 436 | if (mnfst && typeof(mnfst) === "string") { 437 | bldr.include_manifest(mnfst); 438 | changed = true; 439 | } 440 | } 441 | } 442 | } 443 | 444 | if (changed === true) { 445 | fs.ensureDirSync(path.dirname(mnfst_path)); 446 | fs.writeFileSync(mnfst_path, bldr.get(), (err) => { 447 | if (err) { 448 | throw err; 449 | } 450 | }); 451 | } 452 | 453 | return mnfst_path; 454 | } 455 | 456 | 457 | from_template(module, destination) { 458 | 459 | // ToDo: Merge w/ outer functinality 460 | 461 | function check_template(t, section, key) { 462 | if (!t) return false; 463 | 464 | let keys = t[section] ?? [] 465 | for (let i = 0; i < keys.length; i++) { 466 | if (keys[i] === key || keys[i] == "*") { 467 | return true; 468 | } 469 | } 470 | return false; 471 | } 472 | 473 | let self = this; 474 | 475 | // split the module name to get its scope 476 | let scoped_module = module.split("/"); 477 | 478 | // prepare the dir for this manifest 479 | let mnfst_path = path.join(destination, ...scoped_module, "manifest.json"); 480 | fs.ensureDirSync(path.dirname(mnfst_path)); 481 | 482 | // check if there is a template in mcu_nodes 483 | let template_path = path.join(self.mcu_modules_path, ...scoped_module, "manifest.json"); 484 | if (!fs.existsSync(template_path)) 485 | return; 486 | 487 | let mnfst_template = require(template_path); 488 | let template = mnfst_template["//"]?.template; 489 | if (template === undefined) { 490 | // sorry... this is not a template! 491 | return; 492 | } 493 | 494 | let mt = clone(mnfst_template); 495 | delete mt["//"].template; 496 | let bldr = new manifest_builder(self.nodes_library, self.mcu_modules_path, this.options); 497 | bldr.initialize(mt); 498 | 499 | 500 | let _MCUMODULES = false 501 | if (check_template(template, "build", "MCUMODULES")){ 502 | // first: define MCUMODULES 503 | bldr.add_build("MCUMODULES", self.mcu_modules_path); 504 | _MCUMODULES = true; 505 | } 506 | 507 | if (check_template(template, "build", "REDNODES")){ 508 | // resolve core nodes directory => "@node-red/nodes" 509 | for (let i=0; i { 523 | if (err) { 524 | throw err; 525 | } 526 | }); 527 | 528 | let changed = false; 529 | 530 | let deps = template.include ?? []; 531 | for (let key of deps) { 532 | let mnfst = this.get_manifest_of_module(key, destination); 533 | if (mnfst && typeof (mnfst) === "string") { 534 | bldr.include_manifest(mnfst); 535 | changed = true; 536 | continue; 537 | } 538 | mnfst = this.create_manifests_for_module(key, destination); 539 | if (mnfst && typeof(mnfst) === "string") { 540 | bldr.include_manifest(mnfst); 541 | changed = true; 542 | } 543 | } 544 | 545 | if (changed === true) { 546 | fs.ensureDirSync(path.dirname(mnfst_path)); 547 | fs.writeFileSync(mnfst_path, bldr.get(), (err) => { 548 | if (err) { 549 | throw err; 550 | } 551 | }); 552 | } 553 | 554 | for (let file of template.copy ?? []) { 555 | let src = path.resolve(path.dirname(template_path), file); 556 | let to = path.resolve(path.dirname(mnfst_path), file); 557 | fs.copyFileSync(src, to) 558 | } 559 | 560 | return mnfst_path; 561 | 562 | } 563 | 564 | add_build(key, value) { 565 | if (!this.manifest.build) { 566 | this.manifest.build = {} 567 | } 568 | this.manifest.build[key] = value; 569 | } 570 | 571 | add_module(_path, key) { 572 | 573 | key = key ?? "*"; 574 | if (typeof(key) !== "string") throw("typeof(key) must be string.") 575 | 576 | if (!this.manifest) { 577 | throw "Missing manifest @ add_module" 578 | } 579 | 580 | if (!this.manifest.modules) { 581 | this.manifest.modules = { 582 | "*": [], 583 | "~": [] 584 | }; 585 | } 586 | 587 | if (!this.manifest.modules[key]) { 588 | this.manifest.modules[key] = _path; 589 | return true; 590 | } 591 | 592 | let mms = this.manifest.modules[key]; 593 | if (Array.isArray(mms)) { 594 | if (mms.indexOf(_path) < 0) { 595 | mms.push(_path); 596 | return true; 597 | } 598 | } else if (_path !== mms) { 599 | this.manifest.modules[key] = [ mms, _path] 600 | return true; 601 | } 602 | 603 | return false; 604 | } 605 | 606 | add_preload(module) { 607 | if (!this.manifest.preload) { 608 | this.manifest.preload = [] 609 | } 610 | if (this.manifest.preload.indexOf(module) < 0) { 611 | this.manifest.preload.push(module); 612 | } 613 | } 614 | 615 | // create_manifests_from_package 616 | 617 | get() { 618 | return JSON.stringify(this.manifest, null, " "); 619 | } 620 | 621 | add(object, key) { 622 | 623 | // this.manifest[key] ??= {}; // node 15+ 624 | if (!this.manifest[key]) { 625 | this.manifest[key] = {}; 626 | } 627 | 628 | let slot = this.manifest[key]; 629 | let obj = clone(object); 630 | 631 | for (let k in obj) { 632 | slot[k] = obj[k]; 633 | } 634 | 635 | } 636 | } 637 | 638 | 639 | module.exports = { 640 | builder: manifest_builder 641 | } -------------------------------------------------------------------------------- /lib/proxy.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require("events"); 2 | // const net = require('node:net'); // <== Node16 3 | const net = require('net'); // <== Node14 4 | // const X2JS = require("x2js"); 5 | const {XMLParser} = require('fast-xml-parser'); 6 | 7 | 8 | 9 | class mcuProxy extends EventEmitter { 10 | 11 | // This code derives from an idea of @phoddie 12 | 13 | constructor(portIn, portOut, relay, trace) { 14 | 15 | super(); 16 | 17 | this.portIn = portIn || 5004; 18 | this.portOut = portOut || 5002; 19 | this.relay = relay || true; 20 | this.trace = trace || false; 21 | 22 | this.inCache = ''; 23 | this.xsbug; 24 | this.target; 25 | 26 | this.is_connected_timer; 27 | let self = this; 28 | 29 | console.log("Launching mcuProxy:"); 30 | this.server = net.createServer(target => { 31 | 32 | this.target = target; 33 | 34 | if (this.trace) 35 | console.log('mcuProxy: Target connected.'); 36 | 37 | if (this.relay) { 38 | // connect to xsbug to be able to relay messages 39 | 40 | try { 41 | this.xsbug = net.connect({ 42 | port: this.portOut, 43 | host: "127.0.0.1" 44 | }); 45 | } catch (err) { 46 | console.log("- Failed to connect to xsbug: " + err.message); 47 | this.xsbug = undefined; 48 | return; 49 | } 50 | 51 | let xsbug = this.xsbug; 52 | 53 | xsbug.setEncoding("utf8"); 54 | xsbug.on('lookup', (err, address, family, host) => { 55 | if (err) { 56 | console.log(`- Connecting to xsbug: Error while trying to resolve ${host}: ` + err.message); 57 | } else { 58 | console.log(`- Connecting to xsbug: Resolved ${host} to ${address}/${family}.`); 59 | } 60 | }); 61 | xsbug.on("connect", () => { 62 | let c = xsbug.address(); 63 | console.log(`- Connected to xsbug @ ${c.address}:${c.port}/${c.family}.`); 64 | }) 65 | xsbug.on('ready', data => { 66 | while (xsbug.deferred.length) 67 | xsbug.write(xsbug.deferred.shift() + "\r\n"); 68 | delete xsbug.deferred; 69 | }); 70 | xsbug.on('data', data => { 71 | // data = JSON.stringify(data); 72 | if (this.trace) 73 | console.log("mcuProxy: From xsbug => " + data); 74 | 75 | if ("" == data.trim()) { 76 | self.emit("mcu", {state: "abort" }); 77 | } 78 | 79 | target.write(data); 80 | }); 81 | xsbug.on('end', () => { 82 | if (this.trace) 83 | console.log("mcuProxy: xsbug disconnected."); 84 | target.destroy(); 85 | }); 86 | xsbug.on('error', () => { 87 | try { 88 | this.xsbug.destroy(); 89 | } catch(err) {} 90 | this.xsbug = undefined; 91 | }); 92 | xsbug.deferred = []; 93 | xsbug.deferred.push("2"); 94 | } 95 | 96 | target.setEncoding("utf8"); 97 | let first = true; 98 | target.on('data', data => { 99 | 100 | if (self.is_connected_timer) { 101 | clearTimeout(self.is_connected_timer); 102 | } 103 | 104 | self.is_connected_timer = setTimeout(function() { 105 | self.emit("mcu", {state: "abort" }); 106 | self.is_connected_timer = undefined; 107 | }, 2000); 108 | 109 | if (this.trace) { 110 | console.log("mcuProxy: From Target => " + data + "<==="); 111 | } 112 | 113 | this.inCache += data.toString(); 114 | 115 | // parse messages here 116 | // each message is an XML document 117 | // status messages are sent in a bubble right message of the form: 118 | // JSON STATUS MESSAGE HERE 119 | 120 | // xsbug seems to be very sensitive to wrong formatted xml. 121 | // Thus this is the cache of sanitized xml, to be sent later to xsbug 122 | let for_xsbug = []; 123 | 124 | let end = -1; 125 | do { 126 | let parse_error = false; 127 | 128 | const start = this.inCache.indexOf(""); 129 | end = (start < 0) ? -1 : this.inCache.indexOf("", start); 130 | 131 | if (end > -1) { 132 | const xml = this.inCache.slice(start, end + 8); 133 | let doc; 134 | try { 135 | let parser = new XMLParser({ 136 | ignoreAttributes: false 137 | }); 138 | doc = parser.parse(xml) 139 | } 140 | catch { 141 | parse_error = true; 142 | } 143 | 144 | this.inCache = this.inCache.slice(end + 8); 145 | 146 | // ***** 147 | // * This logic supports two mcu communication protocols! 148 | // * 149 | // * #1: status = { ...status, ...source}; node id provided as source.id 150 | // * This protocol has significant overhead, as only source.id will be processed further. 151 | // * 152 | // * #2: status = { ...status }; node id provided as bubble.name (!!) 153 | // * This protocol runs with less overhead but a bit more effort to extract the node id. 154 | // * 155 | // * #1 was the original protocol, #2 introduced in 11/22. 156 | // ***** 157 | 158 | // Template @ fast-xml-parser 159 | // { 160 | // xsbug: { 161 | // bubble: { 162 | // '#text': '{"state": "building"}', 163 | // '@_name': 'NR_EDITOR', 164 | // '@_value': '1', 165 | // '@_path': '[...]/nodered.js', 166 | // '@_line': '147' 167 | // } 168 | // } 169 | // } 170 | 171 | if (doc?.xsbug?.bubble) { 172 | let bbl = doc.xsbug.bubble; 173 | 174 | let id = bbl['@_name']?.length > 0 ? bbl['@_name'] : undefined; 175 | let text = bbl['#text']?.length > 0 ? bbl['#text'] : undefined; 176 | 177 | if (text) { 178 | try { 179 | let msg = JSON.parse(text); 180 | 181 | if ("state" in msg) { 182 | this.emit("mcu", msg); 183 | } else { 184 | let tags = ["status", "input", "error", "warn"]; 185 | for (let i=0, l=tags.length; i 0 && "XS" == login['@_value']) { 210 | this.emit("mcu", {state: "login", from: login['@_name'] ?? ""}); 211 | } 212 | } 213 | 214 | if (!parse_error) 215 | for_xsbug.push(xml); 216 | 217 | } 218 | } while (end >= 0); 219 | 220 | let prxy = this; 221 | for_xsbug.forEach(function(data) { 222 | if (prxy.relay && prxy.xsbug) { 223 | if (prxy.xsbug.deferred) 224 | prxy.xsbug.deferred.push(data); 225 | else 226 | prxy.xsbug.write(data + "\r\n"); 227 | } 228 | else { 229 | if (first) { 230 | // first time need to send set-all-breakpoints as xsbug does 231 | first = false; 232 | target.write('\r\n\r\n'); 233 | } 234 | else { 235 | // assume any other messages are a break, so send go. This isn't always corrrect but may always work. 236 | target.write('\r\n\r\n'); 237 | } 238 | } 239 | }) 240 | 241 | }); 242 | target.on('end', () => { 243 | self.emit("mcu", {state: "abort" }); 244 | if (this.trace) 245 | console.log('mcuProxy: Target disconnected.'); 246 | if (this.xsbug) 247 | this.xsbug.destroy(); 248 | this.xsbug = undefined; 249 | }); 250 | target.on("error", () => { 251 | // we should emit an error here... 252 | }); 253 | target.on("close", () => { 254 | self.emit("mcu", {state: "abort" }); 255 | }) 256 | }); 257 | 258 | this.server.listen(this.portIn, () => { 259 | let addr = this.server.address() 260 | console.log(`- Listening for MCU @ ${addr.address}:${addr.port}/${addr.family}`); 261 | }); 262 | } 263 | 264 | send2mcu(command, flow, node, data) { 265 | 266 | if (this.target) { 267 | let target = this.target; 268 | const options = { 269 | command: command, 270 | flow: flow, 271 | id: node, 272 | data: data 273 | }; 274 | try { 275 | target.write(`\r\n\r\n`); 276 | } 277 | catch (err) { 278 | console.log("Error sending command to MCU: " + err.message); 279 | } 280 | } 281 | } 282 | 283 | disconnect() { 284 | this.emit("mcu", {state: "abort" }); 285 | 286 | if (this.xsbug) { 287 | try { 288 | this.xsbug.destroy(); 289 | } catch {} 290 | } 291 | this.xsbug = undefined; 292 | 293 | if (this.target) { 294 | try { 295 | this.target.destroy(); 296 | } catch {} 297 | } 298 | this.target = undefined; 299 | 300 | if (this.server) { 301 | try { 302 | this.server.close(); 303 | this.server.unref(); 304 | } catch {} 305 | } 306 | this.server = undefined; 307 | 308 | } 309 | } 310 | 311 | module.exports = { 312 | proxy: mcuProxy 313 | } 314 | -------------------------------------------------------------------------------- /lib/relay.js: -------------------------------------------------------------------------------- 1 | class MessageRelay { 2 | 3 | constructor(RED) { 4 | this.RED = RED; 5 | } 6 | 7 | #getNode(id) { 8 | return this.RED.nodes.getNode(id); 9 | } 10 | 11 | status(id, data) { 12 | 13 | /* { 14 | text: 1658087621772, 15 | source: { id: '799b7e8fcf64e1fa', type: 'debug', name: 'debug 4' } 16 | } */ 17 | 18 | let status = {}; 19 | 20 | let fill = data.fill; 21 | let shape = data.shape; 22 | let text = `${data.text}`; // convert any to string 23 | 24 | if (fill) { status["fill"] = fill;} 25 | if (shape) { status["shape"] = shape;} 26 | if (text) { status["text"] = text;} 27 | 28 | if (this.#getNode(id)) { 29 | this.RED.events.emit("node-status",{ 30 | "id": id, 31 | "status": status 32 | }); 33 | } 34 | 35 | } 36 | 37 | input(id, data) { 38 | if (id) 39 | this.#getNode(id)?.receive(data); 40 | } 41 | 42 | error(id, data) { 43 | if (id) 44 | this.#getNode(id)?.error(data.error); 45 | } 46 | 47 | warn(id, data) { 48 | if (id) 49 | this.#getNode(id)?.warn(data.warn); 50 | } 51 | 52 | mcu(data) { 53 | 54 | let MCU_EXPERIMENTAL = process.env['MCU_EXPERIMENTAL']; 55 | 56 | // as the standard interface is (id, data) 57 | // we accept data as the second argument as well - if the first is undefined. 58 | 59 | if (arguments.length > 1) { 60 | if (!arguments[0]) { 61 | data = arguments[1] 62 | } 63 | } 64 | 65 | 66 | let msg; 67 | let options; 68 | 69 | switch (data.state) { 70 | case "login": 71 | 72 | let from = data.from 73 | if (from.length > 0) { 74 | if (from === "main") { 75 | msg = "MCU is initializing..."; 76 | } else if (from.length > 6) { 77 | let c = from.substring(0, 6); 78 | let c_id = from.substring(6); 79 | if (c === "config" && c_id == options.id) { 80 | msg = "Simulator is initializing..."; 81 | } 82 | } 83 | } 84 | 85 | options = { type: "warning", timeout: 5000 }; 86 | break; 87 | 88 | // reset node status 89 | // In case we ever support more than one MCU instance running in parallel, 90 | // we need a more precise way to select the affected nodes. 91 | case "abort": 92 | // this affects only nodes having the reset_status flag set in _mcu 93 | // Att: this flag is set only (!) in the runtime representation => getNode 94 | this.RED.nodes.eachNode((n) => { 95 | if (n._mcu?.mcu) { 96 | let nn = this.RED.nodes.getNode(n.id); 97 | if (nn?._mcu?.reset_status_on_abort) { 98 | this.RED.events.emit("node-status",{ 99 | "id": n.id, 100 | "status": {} 101 | }); 102 | } 103 | } 104 | }) 105 | break; 106 | 107 | case "building": 108 | // this affects all mcu nodes! 109 | console.log("@building"); 110 | this.RED.nodes.eachNode((n) => { 111 | if (n._mcu?.mcu) { 112 | this.RED.events.emit("node-status",{ 113 | "id": n.id, 114 | "status": {} 115 | }); 116 | } 117 | }) 118 | break; 119 | 120 | case "ready": 121 | 122 | // building & ready fire almost simultaneously 123 | msg = "Flows are ready."; 124 | options = { timeout: 5000 }; 125 | break; 126 | 127 | case "mod_waiting": 128 | console.log("@mod_waiting"); 129 | if (MCU_EXPERIMENTAL & 1) { 130 | msg = "Host is ready. Waiting for flows to be installed."; 131 | options = { timeout: 5000 }; 132 | break; 133 | } 134 | 135 | case "mod_ui_missing": 136 | if (MCU_EXPERIMENTAL & 1) { 137 | const notif = RED.notify( 138 | "This flow uses UI nodes, yet the host was build without UI support.
Please ensure that UI Support is enabled, rebuild & install the host, then rebuild this flow.", 139 | options = { 140 | type: "error", 141 | modal: true, 142 | buttons: [ 143 | { 144 | text: "OK", 145 | click: function(e) { 146 | notif.close(); 147 | } 148 | }, 149 | ] 150 | } 151 | ); 152 | break; 153 | } 154 | 155 | case "notify": 156 | msg = data.label; 157 | options = { 158 | type: data.type ?? "compact", 159 | timeout: 5000 160 | }; 161 | break; 162 | 163 | default: 164 | return; 165 | 166 | } 167 | 168 | if (msg && msg.length > 0) { 169 | console.log("@mcu_notify"); 170 | this.RED.comms.publish("mcu/notify", { 171 | "message": msg, 172 | "options": options 173 | }); 174 | } 175 | 176 | } 177 | 178 | } 179 | 180 | module.exports = { 181 | relay: MessageRelay 182 | } -------------------------------------------------------------------------------- /mcu_modules/eta/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "//": { 3 | "***": "https://github.com/ralphwetzel/node-red-mcu-plugin", 4 | "npm": "eta", 5 | "xs": "manifest.json", 6 | "@": "2022-08-23T06:31:49.793Z", 7 | "ref": "https://github.com/Moddable-OpenSource/moddable", 8 | "template": { 9 | "build": ["MCUMODULES"], 10 | "modules": [ "eta" ] 11 | } 12 | }, 13 | "build": {}, 14 | "include": [ 15 | "$(MCUMODULES)/require/manifest.json" 16 | ], 17 | "modules": { 18 | "*": [] 19 | }, 20 | "platforms": { 21 | "...": { 22 | "warning": "eta tends to create a StackOverflow error when compiling the template on MCU" 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /mcu_modules/fs/fs.js: -------------------------------------------------------------------------------- 1 | // import { File } from "file"; 2 | // Att: "file" not loaded as default on esp 3 | 4 | export function readFileSync() { 5 | trace ("@readFileSync", "\n") 6 | } 7 | 8 | export function existsSync() { 9 | trace ("@existsSync", "\n") 10 | } 11 | -------------------------------------------------------------------------------- /mcu_modules/fs/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | ], 4 | "modules": { 5 | "*": [ 6 | "./fs" 7 | ] 8 | }, 9 | "preload": [ 10 | "fs" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /mcu_modules/node-red-contrib-semaphore/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "//": { 3 | "***": "https://github.com/ralphwetzel/node-red-mcu-plugin", 4 | "npm": "node-red-contrib-semaphore", 5 | "xs": "manifest.json", 6 | "@": "2022-10-16T17:27:14.820Z", 7 | "ref": "https://github.com/Moddable-OpenSource/moddable", 8 | "template": { 9 | "build": ["MCUMODULES"], 10 | "modules": [ "node-red-contrib-semaphore" ] 11 | } 12 | }, 13 | "build": { 14 | }, 15 | "include": [ 16 | "$(MCUMODULES)/require/manifest.json", 17 | "$(MCUMODULES)/semaphore/manifest.json" 18 | ], 19 | "modules": { 20 | "*": [] 21 | }, 22 | "preload": [ 23 | "node-red-contrib-semaphore" 24 | ] 25 | } -------------------------------------------------------------------------------- /mcu_modules/node-red-node-serialport/README.md: -------------------------------------------------------------------------------- 1 | ## node-red-node-serialport 2 | 3 | This directory contains a shim for `node-red-node-serialport`. 4 | The code gets - transparently - injected when using `node-red-node-serialport` for an MCU build. 5 | 6 | Currently, `Serial In` - node & `Serial Out` - Node are supported. 7 | `Serial Request` - Node is not supported. 8 | 9 | The major difference to the standard implementation derives from the demand to define the pin parameters of the serial interface to be used by these nodes. 10 | As the property editor doesn't provide a better option, this pin parameter definition has to be entered as _Serial Port path_, e.g. `/P2/R33/T19`. 11 | 12 | serial_port_path 14 | 15 | * `/Px`: Port number 16 | * `/Rx`: Receive (RX) pin number 17 | * `/Tx`: Transmit (TX) pin number 18 | 19 | The order of the _path elements_ doesn't matter. 20 | 21 | > **Attention**: The default serial interface (Port: 1 / RX: Pin 3 / TX: Pin 1) is [reserved for / occupied by the debugging link between the MCU and Node-RED](https://github.com/Moddable-OpenSource/moddable/issues/1226#issuecomment-1823361637)! Thus you must use a different parameter set (and different hardware pins!) to establish a connection to you serial devices. 22 | 23 | This implementaton supports the following properties for a `Serial Port` definition in `node-red-node-serialport`: 24 | 25 | - [x] Baud Rate 26 | - [ ] Data Bits, Parity, Stop Bits: According ECMA-419 version 2, always `8-N-1`. 27 | - [ ] DTR, RTS, CTS, DSR 28 | - [x] Start character 29 | - [x] Split input: character, timeout, silence, length 30 | - [x] Deliver: Binary Buffer, ASCII strings 31 | - [x] Stream of single bytes / chars 32 | - [x] Append output character 33 | - [ ] Request response timeout 34 | 35 | > This shim may be removed as soon as an implementation of `node-red-node-serialport` is incorporated into core `node-red-mcu`. 36 | 37 | 38 | -------------------------------------------------------------------------------- /mcu_modules/node-red-node-serialport/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "//": { 3 | "***": "https://github.com/ralphwetzel/node-red-mcu-plugin", 4 | "npm": "node-red-node-serialport", 5 | "xs": "manifest.json", 6 | "@": "2023-11-21T21:01:25.889Z", 7 | "ref": "https://github.com/Moddable-OpenSource/moddable", 8 | "template": { 9 | "build": ["MCUMODULES"] 10 | } 11 | }, 12 | "build": { 13 | }, 14 | "include": [ 15 | "$(MCUMODULES)/require/manifest.json" 16 | ], 17 | "modules": { 18 | "*": [], 19 | "node-red-node-serialport": "$(MCUMODULES)/node-red-node-serialport/serialport.js" 20 | }, 21 | "preload": [ 22 | "node-red-node-serialport" 23 | ] 24 | } -------------------------------------------------------------------------------- /mcu_modules/node-red-node-serialport/portpath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ralphwetzel/node-red-mcu-plugin/05b86bf13f7eef638d3e1ece7a9320709d28219c/mcu_modules/node-red-node-serialport/portpath.png -------------------------------------------------------------------------------- /mcu_modules/node-red-node-serialport/serialport.js: -------------------------------------------------------------------------------- 1 | /* 2 | node-red-node-serialport / Shim by @ralphwetzel 3 | Copyright 2023 Ralph Wetzel 4 | https://github.com/ralphwetzel/node-red-mcu-plugin 5 | License: MIT 6 | */ 7 | 8 | import { Node } from "nodered"; 9 | import Serial from "embedded:io/serial"; 10 | 11 | // Configuration Node 12 | class mcuSerialPort extends Node { 13 | 14 | static type = "serial-port"; 15 | 16 | #read_timeout; 17 | 18 | onStart(config) { 19 | super.onStart(config); 20 | let self = this; 21 | 22 | self.listeners = []; 23 | self.read_buffer = undefined; 24 | 25 | self.state = { 26 | connected: { 27 | fill: "green", 28 | shape: "dot", 29 | text: "connected" 30 | }, 31 | error: { 32 | fill: "red", 33 | shape: "dot", 34 | text: "Failed to connect" 35 | }, 36 | status: "", 37 | targets: [] 38 | } 39 | 40 | self.newline = config.newline; /* overloaded: split character, timeout, or character count */ 41 | self.addchar = config.addchar || ""; 42 | self.serialbaud = parseInt(config.serialbaud) || 57600; 43 | // self.databits = 8 // NOT SUPPORTED! parseInt(config.databits) || 8; 44 | // self.parity = "none" // config.parity || "none"; 45 | // self.stopbits = 1 // parseInt(config.stopbits) || 1; 46 | // self.dtr = "none" // config.dtr || "none"; 47 | // self.rts = "none" // config.rts || "none"; 48 | // self.cts = "none" // config.cts || "none"; 49 | // self.dsr = "none" // config.dsr || "none"; 50 | self.bin = config.bin || "false"; 51 | self.out = config.out || "char"; 52 | // self.responsetimeout = config.responsetimeout || 10000; 53 | 54 | let convert = function(input) { 55 | // from 25-serial.js: 56 | input = input.replace("\\n","\n").replace("\\r","\r").replace("\\t","\t").replace("\\e","\e").replace("\\f","\f").replace("\\0","\0"); // jshint ignore:line 57 | if (input.substr(0,2) == "0x") { 58 | input = parseInt(input,16); 59 | } else { 60 | if (input.length > 0) { 61 | input = input.charCodeAt(0); 62 | } 63 | } 64 | return input; 65 | } 66 | 67 | self.waitfor = convert(config.waitfor); 68 | self.addchar = convert(config.addchar) || ""; 69 | 70 | switch (self.out) { 71 | case "char": { 72 | self.newline = convert(config.newline); 73 | break; 74 | } 75 | default: { 76 | self.newline = Number(config.newline); 77 | } 78 | } 79 | 80 | let parts = config.serialport.split("/"); 81 | let res = { 82 | "P": 1, 83 | "T": 1, 84 | "R": 3 85 | }; 86 | 87 | Object.keys(res).forEach( k => { 88 | for (let i=0; i 0) { 123 | for (let i=0;i { 129 | data.forEach( (d) => { 130 | let msg = {}; 131 | msg._msgid ??= RED.util.generateId(); 132 | msg.payload = d; 133 | RED.mcu.enqueue(msg, l); 134 | }) 135 | }) 136 | } 137 | 138 | let processor = { 139 | "char": function (buf) { 140 | 141 | let forward = []; 142 | let start = 0; 143 | let split; 144 | 145 | if (self.read_buffer?.length) { 146 | buf = concat(self.read_buffer, buf); 147 | } 148 | let length = buf.length; 149 | 150 | do { 151 | split = buf.indexOf(self.newline, start); 152 | if (split > -1) { 153 | forward.push(buf.slice(start, split)); 154 | start = split + 1; 155 | } 156 | } while (split > -1 && start < length) 157 | 158 | self.read_buffer = (start < length) ? new Uint8Array(buf.buffer, start) : undefined; 159 | if (forward.length > 0) { 160 | send_buffer(forward); 161 | } 162 | }, 163 | "count": function(buf) { 164 | 165 | let forward = []; 166 | let start = 0; 167 | 168 | if (self.read_buffer?.length) { 169 | buf = concat(self.read_buffer, buf); 170 | } 171 | let length = buf.length; 172 | 173 | while (start + self.newline < length) { 174 | forward.push(buf.slice(start, start + self.newline)); 175 | start += self.newline; 176 | } 177 | 178 | self.read_buffer = (start < length) ? new Uint8Array(buf.buffer, start) : undefined; 179 | 180 | if (forward.length > 0) { 181 | send_buffer(forward); 182 | } 183 | }, 184 | "time": function(buf) { 185 | 186 | if (self.read_buffer?.length) { 187 | self.read_buffer = concat(self.read_buffer, buf); 188 | } else { 189 | self.read_buffer = buf; 190 | } 191 | 192 | if (!self.#read_timeout) { 193 | self.#read_timeout = setTimeout(function () { 194 | self.#read_timeout = undefined; 195 | let forward = self.read_buffer; 196 | self.read_buffer = undefined; 197 | send_buffer([forward]); 198 | }, self.newline); 199 | } 200 | }, 201 | "interbyte": function (buf) { 202 | 203 | if (self.read_buffer?.length) { 204 | self.read_buffer = concat(self.read_buffer, buf); 205 | } else { 206 | self.read_buffer = buf; 207 | } 208 | 209 | if (self.#read_timeout) { 210 | clearTimeout(self.#read_timeout); 211 | } 212 | self.#read_timeout = setTimeout(function () { 213 | self.#read_timeout = undefined; 214 | let forward = self.read_buffer; 215 | self.read_buffer = undefined; 216 | send_buffer([forward]); 217 | }, self.newline); 218 | }, 219 | "pass": function (buf) { 220 | 221 | // explode the buffer into arrays of single element 222 | let forward = []; 223 | buf.forEach( (value) => { 224 | forward.push(new Uint8Array([value])); 225 | }); 226 | 227 | send_buffer(forward); 228 | } 229 | } 230 | 231 | // configure the processor! 232 | let process_on_read = processor[self.out] ?? function () {} 233 | if (self.newline == 0 || self.newline == "") { 234 | process_on_read = processor["pass"]; 235 | } 236 | 237 | try { 238 | self.serial = new Serial({ 239 | baud: parseInt(config.serialbaud) || 115200, 240 | port: res.P, 241 | receive: res.R, 242 | transmit: res.T, 243 | format: "buffer", 244 | onReadable: function (count) { 245 | let buf = new Uint8Array(this.read()); 246 | if (self.waitfor) { 247 | let start = buf.indexOf(self.waitfor); 248 | if (start > -1) { 249 | self.waitfor = undefined; 250 | if (buf.length > start + 1) { 251 | process_on_read(buf.slice(start + 1)); 252 | } 253 | } 254 | return; 255 | } 256 | process_on_read(buf); 257 | } 258 | }); 259 | self.state.status = "connected"; 260 | } catch (err) { 261 | self.error(err.toString()); 262 | self.state.status = "error"; 263 | } 264 | 265 | self.register_listener = function(id) { 266 | let n = RED.nodes.getNode(id); 267 | if (n) { 268 | self.listeners.push(n); 269 | } 270 | } 271 | 272 | self.register_status = function(id) { 273 | let n = RED.nodes.getNode(id); 274 | if (n) { 275 | self.state.targets.push(n); 276 | } 277 | } 278 | 279 | self.write = function(buf) { 280 | try { 281 | self.serial.write(buf); 282 | } catch {} 283 | } 284 | 285 | self.ping = function() { 286 | let s = self.state[self.state.status]; 287 | if (s) { 288 | self.state.targets.forEach( (n) => { 289 | n.status(s); 290 | }) 291 | } 292 | } 293 | 294 | self.ping(); 295 | setInterval(function() { 296 | self.ping(); 297 | }, 2500); 298 | } 299 | 300 | static { 301 | RED.nodes.registerType(this.type, this); 302 | } 303 | 304 | } 305 | 306 | 307 | class mcuSerialOut extends Node { 308 | 309 | static type = "serial out"; 310 | 311 | onStart(config) { 312 | super.onStart(config); 313 | this.serialConfig = config.serial; 314 | 315 | this.serial = RED.nodes.getNode(this.serialConfig); 316 | if (this.serial) { 317 | this.serial.register_status(this.id); 318 | } 319 | 320 | } 321 | 322 | onMessage(msg, done) { 323 | 324 | let self = this; 325 | 326 | if (!self.serial) { 327 | self.serial = RED.nodes.getNode(this.serialConfig); 328 | } 329 | 330 | if (self.serial) { 331 | self.serial.write(ArrayBuffer.fromString(msg.payload)); 332 | } 333 | 334 | done(); 335 | } 336 | 337 | static { 338 | RED.nodes.registerType(this.type, this); 339 | } 340 | 341 | } 342 | 343 | 344 | class mcuSerialIn extends Node { 345 | 346 | static type = "serial in"; 347 | 348 | onStart(config) { 349 | super.onStart(config); 350 | this.serialConfig = config.serial; 351 | 352 | this.serial = RED.nodes.getNode(this.serialConfig); 353 | if (this.serial) { 354 | this.serial.register_listener(this.id); 355 | this.serial.register_status(this.id); 356 | } 357 | 358 | this.status({fill:"grey",shape:"dot",text:"not connected"}) 359 | 360 | } 361 | 362 | onMessage(msg, done) { 363 | this.send(msg); 364 | done(); 365 | } 366 | 367 | static { 368 | RED.nodes.registerType(this.type, this); 369 | } 370 | 371 | } -------------------------------------------------------------------------------- /mcu_modules/path/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "../require/manifest.json" 4 | ], 5 | "modules": { 6 | "*": [ 7 | "./path" 8 | ] 9 | }, 10 | "preload": [ 11 | "path" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /mcu_modules/path/path.js: -------------------------------------------------------------------------------- 1 | // import { File } from "file"; 2 | // Att: "file" not loaded as default on esp 3 | 4 | export function resolve() { 5 | 6 | } 7 | 8 | export function dirname() { 9 | 10 | } 11 | 12 | export function extname() { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /mcu_modules/require/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "../path/manifest.json", 4 | "../fs/manifest.json" 5 | ], 6 | "modules": { 7 | "*": [ 8 | "./require" 9 | ] 10 | }, 11 | "preload": [ 12 | "require" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /mcu_modules/require/require.js: -------------------------------------------------------------------------------- 1 | import Modules from "modules"; 2 | 3 | function require(module) { 4 | 5 | let req = Modules.importNow(module); 6 | 7 | if (req?.default) 8 | return req.default; 9 | 10 | return req; 11 | 12 | } 13 | 14 | globalThis.require = require; 15 | -------------------------------------------------------------------------------- /mcu_modules/semaphore/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "//": { 3 | "***": "https://github.com/ralphwetzel/node-red-mcu-plugin", 4 | "npm": "semaphore", 5 | "xs": "manifest.json", 6 | "@": "2022-10-16T17:27:14.822Z", 7 | "ref": "https://github.com/Moddable-OpenSource/moddable" 8 | }, 9 | "build": {}, 10 | "include": [], 11 | "modules": { 12 | "*": [], 13 | "semaphore": "./semaphore" 14 | }, 15 | "preload": [ 16 | "semaphore" 17 | ] 18 | } -------------------------------------------------------------------------------- /mcu_modules/semaphore/semaphore.js: -------------------------------------------------------------------------------- 1 | /* 2 | This module is based on semaphore.js: 3 | 4 | * 5 | * @github https://github.com/abrkn/semaphore.js 6 | * @author @abrkn 7 | * @license MIT 8 | * @version 67e14f8 on 20 May 2020 9 | * 10 | 11 | Adaptations to node-red-mcu by @ralphwetzel 12 | https://github.com/ralphwetzel/node-red-ds2482-mcu 13 | License: MIT 14 | */ 15 | 16 | 'use strict'; 17 | 18 | var nextTick = function (fn) { setTimeout(fn, 0); } 19 | if (typeof process != 'undefined' && process && typeof process.nextTick == 'function') { 20 | // node.js and the like 21 | nextTick = process.nextTick; 22 | } 23 | 24 | function semaphore(capacity) { 25 | var semaphore = { 26 | capacity: capacity || 1, 27 | current: 0, 28 | queue: [], 29 | firstHere: false, 30 | 31 | take: function() { 32 | if (semaphore.firstHere === false) { 33 | semaphore.current++; 34 | semaphore.firstHere = true; 35 | var isFirst = 1; 36 | } else { 37 | var isFirst = 0; 38 | } 39 | var item = { n: 1 }; 40 | 41 | if (typeof arguments[0] == 'function') { 42 | item.task = arguments[0]; 43 | } else { 44 | item.n = arguments[0]; 45 | } 46 | 47 | if (arguments.length >= 2) { 48 | if (typeof arguments[1] == 'function') item.task = arguments[1]; 49 | else item.n = arguments[1]; 50 | } 51 | 52 | var task = item.task; 53 | item.task = function() { task(semaphore.leave); }; 54 | 55 | if (semaphore.current + item.n - isFirst > semaphore.capacity) { 56 | if (isFirst === 1) { 57 | semaphore.current--; 58 | semaphore.firstHere = false; 59 | } 60 | return semaphore.queue.push(item); 61 | } 62 | 63 | semaphore.current += item.n - isFirst; 64 | item.task(semaphore.leave); 65 | if (isFirst === 1) semaphore.firstHere = false; 66 | }, 67 | 68 | leave: function(n) { 69 | n = n || 1; 70 | 71 | semaphore.current -= n; 72 | 73 | if (!semaphore.queue.length) { 74 | if (semaphore.current < 0) { 75 | throw new Error('leave called too many times.'); 76 | } 77 | 78 | return; 79 | } 80 | 81 | var item = semaphore.queue[0]; 82 | 83 | if (item.n + semaphore.current > semaphore.capacity) { 84 | return; 85 | } 86 | 87 | semaphore.queue.shift(); 88 | semaphore.current += item.n; 89 | 90 | nextTick(item.task); 91 | }, 92 | 93 | available: function(n) { 94 | n = n || 1; 95 | return(semaphore.current + n <= semaphore.capacity); 96 | } 97 | }; 98 | 99 | return semaphore; 100 | }; 101 | 102 | export { semaphore as default } 103 | -------------------------------------------------------------------------------- /mods.md: -------------------------------------------------------------------------------- 1 | # Mods Support in node-red-mcu-plugin 2 | 3 | This document intends to describe the current status of Mods support in `node-red-mcu-plugin`. 4 | 5 | Mods support currently is an experimental feature. To enable it, you have to define 6 | 7 | ``` js 8 | process.env.MCU_EXPERIMENTAL = 1 9 | ``` 10 | 11 | in your `settings.js` definition file. 12 | 13 | ## Build mode 14 | The Build Configuration Panel now offers a new defintion property, called `Build Mode` - right below the Build button. You may select here to perform either a full build (the current standard) or to enable Mod Support: 15 | 16 | build_mode_button 18 | 19 | 20 | ## Host system setup 21 | To run mods on an MCU you first have to build & flash the Host system to the MCU. 22 | This - usually - needs to be done only once. 23 | 24 | After selecting `Mod Support` as build mode, the dropdown menu of the Build button provides a dedicated action to build this Host system: 25 | 26 | build_host.png 28 | 29 | The host will report ready & waiting for the upload of flows when build & flashed successfully. 30 | 31 | ## Building Mods 32 | After selecting `Mod Support` as build mode, building Mods is as easy as clicking the Build button. 33 | 34 | ## ToDos 35 | - [ ] Provide better creation params to give the mod enough space to be uploaded. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ralphwetzel/node-red-mcu-plugin", 3 | "version": "1.5.3", 4 | "description": "Plugin to integrate Node-RED MCU Edition into the Node-RED Editor", 5 | "node-red": { 6 | "version": ">=2.0.0 <4.1.0", 7 | "plugins": { 8 | "mcu": "mcu_plugin.js" 9 | } 10 | }, 11 | "scripts": { 12 | "install": "node pullmcu.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/ralphwetzel/node-red-mcu-plugin.git" 17 | }, 18 | "keywords": [ 19 | "node-red", 20 | "node-red-mcu", 21 | "Node-RED", 22 | "Node-RED MCU Edition", 23 | "node-red-mcu-plugin" 24 | ], 25 | "dependencies": { 26 | "clone": "^2.1.2", 27 | "eta": "^2.0.0", 28 | "fast-xml-parser": "^4.0.12", 29 | "fs-extra": "^10.1.0", 30 | "log4js": "^6.7.0", 31 | "nanoid": "<4", 32 | 33 | 34 | "resolve-package-path": "^4.0.3", 35 | "serialport": "^10.4.0", 36 | 37 | "node-abort-controller": "^3.0.0" 38 | }, 39 | "engines": { 40 | "node": ">=14.17.0" 41 | }, 42 | "author": "Ralph Wetzel", 43 | "license": "MIT", 44 | "bugs": { 45 | "url": "https://github.com/ralphwetzel/node-red-mcu-plugin/issues" 46 | }, 47 | "homepage": "https://github.com/ralphwetzel/node-red-mcu-plugin#readme" 48 | } 49 | -------------------------------------------------------------------------------- /package.md: -------------------------------------------------------------------------------- 1 | /* 2 | node-red-mcu-plugin by @ralphwetzel 3 | https://github.com/ralphwetzel/node-red-mcu-plugin 4 | License: MIT 5 | */ 6 | 7 | Comments on package.json 8 | 9 | ```json 10 | "dependencies": { 11 | "node-abort-controller": "^3.0.0" // node@14: Polyfill for AbortController 12 | }, 13 | "engines": { 14 | "node": ">=14.17.0" // to support AbortError @ exec & execFile 15 | } 16 | ``` -------------------------------------------------------------------------------- /pullmcu.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process'); 2 | const fs = require('fs-extra'); 3 | 4 | if (fs.existsSync("./node-red-mcu")) { 5 | execSync("git pull"); 6 | } else { 7 | execSync("git clone https://github.com/phoddie/node-red-mcu.git node-red-mcu"); 8 | } 9 | 10 | return; -------------------------------------------------------------------------------- /resources/build_host.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ralphwetzel/node-red-mcu-plugin/05b86bf13f7eef638d3e1ece7a9320709d28219c/resources/build_host.png -------------------------------------------------------------------------------- /resources/build_mode_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ralphwetzel/node-red-mcu-plugin/05b86bf13f7eef638d3e1ece7a9320709d28219c/resources/build_mode_button.png -------------------------------------------------------------------------------- /resources/junctiontest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ralphwetzel/node-red-mcu-plugin/05b86bf13f7eef638d3e1ece7a9320709d28219c/resources/junctiontest.png -------------------------------------------------------------------------------- /resources/lib/flashTab.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This code copied over from node-red: 3 | * packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js 4 | 5 | * * 6 | * Copyright JS Foundation and other contributors, http://js.foundation 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | **/ 20 | 21 | 22 | let flashingTab; 23 | let flashingTabTimer; 24 | 25 | function flashTab(tabId, className, duration) { 26 | 27 | className = className || "highlighted"; 28 | duration = duration || 2200; 29 | 30 | if(flashingTab && flashingTab.length) { 31 | //cancel current flashing node before flashing new node 32 | clearInterval(flashingTabTimer); 33 | flashingTabTimer = null; 34 | 35 | let fc = flashingTab.data("flashClass"); 36 | fc = fc || className; // wild guess! 37 | 38 | flashingTab.removeClass(fc); 39 | flashingTab = null; 40 | } 41 | let tab = $("#red-ui-tab-" + tabId); 42 | if(!tab || !tab.length) { return; } 43 | 44 | flashingTabTimer = setInterval(function(flashEndTime) { 45 | if (flashEndTime >= Date.now()) { 46 | const highlighted = tab.hasClass(className); 47 | tab.toggleClass(className, !highlighted) 48 | } else { 49 | clearInterval(flashingTabTimer); 50 | flashingTabTimer = null; 51 | flashingTab = null; 52 | tab.removeClass(className); 53 | } 54 | }, 100, Date.now() + duration); 55 | 56 | flashingTab = tab; 57 | 58 | tab.data("flashClass", className); 59 | tab.addClass(className); 60 | } -------------------------------------------------------------------------------- /resources/lib/xsbug.js: -------------------------------------------------------------------------------- 1 | /* 2 | part of the Moddable SDK 3 | https://github.com/phoddie/runmod/tree/master/html 4 | */ 5 | 6 | class XsbugConnection { 7 | constructor(uri) { 8 | this.requestID = 1; 9 | this.pending = []; 10 | } 11 | // transmit xsbug message 12 | doClearBreakpoint(path, line) { 13 | this.sendCommand(``); 14 | } 15 | doGo() { 16 | this.sendCommand(""); 17 | } 18 | doScript(msg) { 19 | this.sendCommand(``); 20 | } 21 | doSetBreakpoint(path, line) { 22 | this.sendCommand(``); 23 | } 24 | doSelect(value) { 25 | this.sendCommand(`