├── .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 |
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 |
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 |
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 |
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 |
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 |
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(``);
26 | }
27 | doSetAllBreakpoints(breakpoints = [], exceptions = true, start = false) {
28 | breakpoints = breakpoints.map(b => ``);
29 | if (exceptions)
30 | breakpoints.unshift('')
31 | if (start)
32 | breakpoints.unshift('')
33 | this.sendCommand(`${breakpoints.join("")}`);
34 | }
35 | doStep() {
36 | this.sendCommand("");
37 | }
38 | doStepInside() {
39 | this.sendCommand("");
40 | }
41 | doStepOutside() {
42 | this.sendCommand("");
43 | }
44 | doToggle(value) {
45 | this.sendCommand(``);
46 | }
47 |
48 | // transmit host messages
49 | doGetPreference(domain, key, callback) {
50 | const byteLength = domain.length + 1 + key.length + 1;
51 | const payload = new Uint8Array(byteLength);
52 | let j = 0;
53 | for (let i = 0; i < domain.length; i++)
54 | payload[j++] = domain.charCodeAt(i);
55 | j++;
56 | for (let i = 0; i < key.length; i++)
57 | payload[j++] = key.charCodeAt(i);
58 |
59 | this.sendBinaryCommand(6, payload, callback);
60 | }
61 | doInstall(data, callback) {
62 | let offset = 0;
63 |
64 | let sendOne = (max) => {
65 | const use = Math.min(max, data.byteLength - offset);
66 | const payload = new Uint8Array(4 + use);
67 | payload[0] = (offset >> 24) & 0xff;
68 | payload[1] = (offset >> 16) & 0xff;
69 | payload[2] = (offset >> 8) & 0xff;
70 | payload[3] = offset & 0xff;
71 | payload.set(new Uint8Array(data, offset, use), 4);
72 | this.sendBinaryCommand(3, payload, function(code) {
73 | offset += max;
74 | if (offset >= data.byteLength) {
75 | if (callback)
76 | callback(code);
77 | return;
78 | }
79 | sendOne(1024);
80 | });
81 | }
82 | sendOne(16);
83 | }
84 | doLoadModule(name, callback) {
85 | const payload = new Uint8Array(name.length + 1);
86 | for (let i = 0; i < name.length; i++)
87 | payload[i] = name.charCodeAt(i);
88 |
89 | this.sendBinaryCommand(10, payload, callback);
90 | }
91 | doRestart() {
92 | this.sendBinaryCommand(1);
93 | this.reset();
94 | }
95 | doSetBaud(baud, callback) {
96 | const payload = new DataView(new ArrayBuffer(4));
97 | payload.setUint32(0, baud, false); // big endian
98 | this.sendBinaryCommand(8, payload, msg => {
99 | this.usb.controlTransferOut({
100 | requestType: 'vendor',
101 | recipient: 'device',
102 | request: 0x01, // SET_BAUDRATE
103 | index: 0x00,
104 | value: 3686400 / baud,
105 | })
106 | .then(() => {
107 | if (callback)
108 | callback.call(this, msg);
109 | }, () => {debugger;});
110 | });
111 | }
112 | doSetPreference(domain, key, value) { // assumes 7 bit ASCII values
113 | const byteLength = domain.length + 1 + key.length + 1 + value.length + 1;
114 | const payload = new Uint8Array(byteLength);
115 | let j = 0;
116 | for (let i = 0; i < domain.length; i++)
117 | payload[j++] = domain.charCodeAt(i);
118 | j++;
119 | for (let i = 0; i < key.length; i++)
120 | payload[j++] = key.charCodeAt(i);
121 | j++;
122 | for (let i = 0; i < value.length; i++)
123 | payload[j++] = value.charCodeAt(i);
124 |
125 | this.sendBinaryCommand(4, payload);
126 | }
127 | doUninstall(callback) {
128 | this.sendBinaryCommand(2, undefined, callback);
129 | }
130 |
131 | // receive messages
132 | onBreak(msg) {}
133 | onLogin(msg) {}
134 | onInstrumentationConfigure(msg) {}
135 | onInstrumentationSamples(msg) {}
136 | onLocal(msg) {}
137 | onLog(msg) {}
138 |
139 | // helpers
140 | sendCommand(msg) {
141 | this.send(XsbugConnection.crlf + msg + XsbugConnection.crlf);
142 | }
143 | sendBinaryCommand(command, payload, callback) {
144 |
145 | // https://github.com/phoddie/runmod#commands
146 |
147 | if (payload) {
148 | if (!(payload instanceof ArrayBuffer))
149 | payload = payload.buffer;
150 | }
151 | let needed = 1;
152 | if (payload)
153 | needed += 2 + payload.byteLength;
154 | else if (callback)
155 | needed += 2;
156 | const msg = new Uint8Array(needed);
157 | msg[0] = command;
158 | if (callback) {
159 | msg[1] = this.requestID >> 8;
160 | msg[2] = this.requestID & 0xff;
161 | this.pending.push({callback, id: this.requestID++});
162 | }
163 | if (payload)
164 | msg.set(new Uint8Array(payload), 3);
165 | this.send(msg.buffer);
166 | }
167 | onReceive(data) {
168 | if ("string" === typeof data) {
169 | const msg = new XsbugMessage((new DOMParser).parseFromString(data, "application/xml"));
170 | console.log(msg);
171 | if (msg.break)
172 | this.onBreak(msg);
173 | else if (msg.login)
174 | this.onLogin(msg);
175 | else if (msg.instruments)
176 | this.onInstrumentationConfigure(msg);
177 | else if (msg.local)
178 | this.onLocal(msg);
179 | else if (msg.log)
180 | this.onLog(msg);
181 | else if (msg.samples)
182 | this.onInstrumentationSamples(msg);
183 | else
184 | debugger; // unhandled
185 | }
186 | else {
187 | const view = new DataView(data);
188 | switch (view.getUint8(0)) {
189 | case 5:
190 | const id = view.getUint16(1), code = view.getInt16(3);
191 | const index = this.pending.findIndex(pending => id === pending.id)
192 | if (index >= 0) {
193 | const pending = this.pending[index];
194 | this.pending.splice(index, 1);
195 | (pending.callback)(code, data.slice(5));
196 | }
197 | break;
198 | default:
199 | debugger;
200 | break;
201 | }
202 | }
203 | }
204 | }
205 | XsbugConnection.crlf = String.fromCharCode(13) + String.fromCharCode(10);
206 |
207 | class XsbugWebSocket extends XsbugConnection {
208 | constructor(uri) {
209 | super();
210 |
211 | this.ws = new WebSocket(uri, "x-xsbug");
212 |
213 | this.ws.onopen = this.onopen.bind(this);
214 | this.ws.onclose = this.onclose.bind(this);
215 | this.ws.onerror = this.onerror.bind(this);
216 | this.ws.onmessage = this.onmessage.bind(this);
217 |
218 | this.ws.binaryType = "arraybuffer";
219 | }
220 | onopen() {
221 | console.log("WS OPEN");
222 | }
223 | onclose() {
224 | if (this.ws)
225 | console.log("WS CLOSE");
226 | }
227 | onerror() {
228 | if (this.ws)
229 | console.log("WS ERROR");
230 | }
231 | onmessage(event) {
232 | console.log("WS RECEIVE " + event.data);
233 | this.onReceive(event.data);
234 | }
235 | send(data) {
236 | console.log("WS SEND " + data);
237 | return this.ws.send(data);
238 | }
239 | disconnect() {
240 | if (this.ws)
241 | this.ws.close();
242 | delete this.ws;
243 | }
244 | }
245 |
246 | // SiLabs controlTransfer documentation: https://www.silabs.com/documents/public/application-notes/AN571.pdf
247 | const filters = [{ 'vendorId': 0x10c4, 'productId': 0xea60 }];
248 |
249 | const DTR = Object.freeze({
250 | CLEAR: 0,
251 | SET: 1,
252 | MASK: 1 << 8
253 | });
254 | const RTS = Object.freeze({
255 | CLEAR: 0,
256 | SET: 2,
257 | MASK: 2 << 8
258 | });
259 |
260 | /*
261 | xsbug can send a lot of data. WebUSB doesn't seem to buffer much.
262 | The implementation keeps multiple reads pendinng to try to avoid dropped data.
263 | */
264 | class XsbugUSB extends XsbugConnection {
265 | constructor(options = {}) {
266 | super();
267 | this.baud = options.baud || 921600;
268 | this.dst = new Uint8Array(32768);
269 | this.connect();
270 | }
271 | reset() {
272 | this.binary = false;
273 | this.dstIndex = 0;
274 | this.currentMachine = undefined;
275 | }
276 | async connect() {
277 | try {
278 | this.reset();
279 |
280 | await this.getDevice();
281 | await this.openDevice();
282 | await this.readLoop();
283 | }
284 | catch (e) {
285 | console.log("Connect error: ", e.message);
286 | if (e.NETWORK_ERR === e.code)
287 | console.log(" ** Looks like you need to uninstall the driver **");
288 | }
289 | }
290 | async getDevice() {
291 | // let devices = await navigator.usb.getDevices();
292 | // if (devices.length > 0) {
293 | // const usb = devices[0];
294 | // this.usb = usb;
295 | // const endpoints = usb.configurations[0].interfaces[0].alternates[0].endpoints;
296 | // let inEndpoint, outEndpoint;
297 | // for (let i = 0; i < endpoints.length; i++) {
298 | // if ("out" === endpoints[i].direction)
299 | // outEndpoint = endpoints[i].endpointNumber;
300 | // if ("in" === endpoints[i].direction)
301 | // inEndpoint = endpoints[i].endpointNumber;
302 | // }
303 | // if ((undefined === inEndpoint) || (undefined === outEndpoint))
304 | // throw new Error("can't find endpoints");
305 | // this.inEndpoint = inEndpoint;
306 | // this.outEndpoint = outEndpoint;
307 | // return usb;
308 | // }
309 | // this.usb = await navigator.usb.requestDevice({ filters });
310 | this.usb = await navigator.serial.requestPort({});
311 | }
312 | async openDevice() {
313 | await this.usb.open();
314 | await this.usb.selectConfiguration(1);
315 |
316 | console.log(this.usb);
317 | await this.usb.claimInterface(0);
318 |
319 | await this.usb.controlTransferOut({
320 | requestType: 'vendor',
321 | recipient: 'device',
322 | request: 0x00, // IFC_ENABLE
323 | index: 0x00,
324 | value: 0x01
325 | });
326 | await this.usb.controlTransferOut({
327 | requestType: 'vendor',
328 | recipient: 'device',
329 | request: 0x07, // SET_MHS
330 | index: 0x00,
331 | value: DTR.MASK | RTS.MASK | RTS.SET | DTR.CLEAR,
332 | });
333 | await this.usb.controlTransferOut({
334 | requestType: 'vendor',
335 | recipient: 'device',
336 | request: 0x01, // SET_BAUDRATE
337 | index: 0x00,
338 | value: 3686400 / this.baud,
339 | });
340 | await this.usb.controlTransferOut({
341 | requestType: 'vendor',
342 | recipient: 'device',
343 | request: 0x12, // PURGE
344 | index: 0x00,
345 | value: 0x0f, // transmit & receive
346 | });
347 | await new Promise(resolve => setTimeout(resolve, 100));
348 | await this.usb.controlTransferOut({
349 | requestType: 'vendor',
350 | recipient: 'device',
351 | request: 0x07, // SET_MHS
352 | index: 0x00,
353 | value: RTS.MASK | RTS.CLEAR,
354 | });
355 | }
356 | async readLoop() {
357 | try {
358 | const byteLength = 8192;
359 | const results = [
360 | this.usb.transferIn(this.inEndpoint, byteLength),
361 | this.usb.transferIn(this.inEndpoint, byteLength),
362 | this.usb.transferIn(this.inEndpoint, byteLength),
363 | ];
364 | let phase = 0;
365 | while (true) {
366 | const result = await results[phase];
367 | results[phase] = this.usb.transferIn(this.inEndpoint, byteLength);
368 | phase = (phase + 1) % results.length;
369 | tracePacket("> ", result.data.buffer);
370 | this.usbReceive(new Uint8Array(result.data.buffer));
371 | }
372 | }
373 | catch (e) {
374 | console.log("readLoop exception: " + e);
375 | }
376 | }
377 | async send(data) {
378 | if ("string" == typeof data) {
379 | const preamble = XsbugConnection.crlf + `` + XsbugConnection.crlf;
380 | data = new TextEncoder().encode(preamble + data);
381 | tracePacket("<", data);
382 | await this.usb.transferOut(this.outEndpoint, data);
383 | }
384 | else {
385 | let preamble = XsbugConnection.crlf + ``;
386 | preamble = new TextEncoder().encode(preamble);
387 | let payload = new Uint8Array(data);
388 | let buffer = new Uint8Array(preamble.length + 2 + payload.length);
389 | buffer.set(preamble, 0);
390 | buffer[preamble.length] = (payload.length >> 8) & 0xff;
391 | buffer[preamble.length + 1] = payload.length & 0xff;
392 | buffer.set(payload, preamble.length + 2);
393 |
394 | tracePacket("< ", buffer);
395 | await this.usb.transferOut(this.outEndpoint, buffer.buffer);
396 | }
397 | }
398 | async disconnect() {
399 | if (this.usb) {
400 | await this.usb.close();
401 | delete this.usb;
402 | }
403 | }
404 |
405 | usbReceive(src) {
406 | const mxTagSize = 17;
407 |
408 | let dst = this.dst;
409 | let dstIndex = this.dstIndex;
410 | let srcIndex = 0, machine;
411 |
412 | while (srcIndex < src.length) {
413 | if (dstIndex === dst.length) { // grow buffer
414 | dst = new Uint8Array(dst.length + 32768);
415 | dst.set(this.dst);
416 | this.dst = dst;
417 | }
418 | dst[dstIndex++] = src[srcIndex++];
419 |
420 | if (this.binary) {
421 | if (dstIndex < 2)
422 | this.binaryLength = dst[0] << 8;
423 | else if (2 === dstIndex)
424 | this.binaryLength |= dst[1];
425 | if ((2 + this.binaryLength) === dstIndex) {
426 | this.onReceive(dst.slice(2, 2 + this.binaryLength).buffer);
427 |
428 | dstIndex = 0;
429 | this.binary = false;
430 | delete this.binaryLength;
431 | }
432 | }
433 | else if ((dstIndex >= 2) && (dst[dstIndex - 2] == 13) && (dst[dstIndex - 1] == 10)) {
434 | if ((dstIndex >= mxTagSize) && (machine = XsbugUSB.matchProcessingInstruction(dst.subarray(dstIndex - mxTagSize, dstIndex)))) {
435 | if (machine.flag)
436 | this.currentMachine = machine.value;
437 | else
438 | this.currentMachine = undefined;
439 | this.binary = machine.binary;
440 | }
441 | else if ((dstIndex >= 10) && (dst[dstIndex - 10] == '<'.charCodeAt()) &&
442 | (dst[dstIndex - 9] == '/'.charCodeAt()) && (dst[dstIndex - 8] == 'x'.charCodeAt()) &&
443 | (dst[dstIndex - 7] == 's'.charCodeAt()) && (dst[dstIndex - 6] == 'b'.charCodeAt()) &&
444 | (dst[dstIndex - 5] == 'u'.charCodeAt()) && (dst[dstIndex - 4] == 'g'.charCodeAt()) &&
445 | (dst[dstIndex - 3] == '>'.charCodeAt())) {
446 | const message = new TextDecoder().decode(dst.subarray(0, dstIndex));
447 | console.log(message);
448 | this.onReceive(message);
449 | }
450 | else {
451 | dst[dstIndex - 2] = 0;
452 | //@@ if (offset > 2) fprintf(stderr, "%s\n", self->buffer);
453 | }
454 | dstIndex = 0;
455 |
456 | }
457 | }
458 |
459 | this.dstIndex = dstIndex;
460 | }
461 | static matchProcessingInstruction(dst) {
462 | let flag, binary = false, value = 0;
463 | if (dst[0] != '<'.charCodeAt())
464 | return;
465 | if (dst[1] != '?'.charCodeAt())
466 | return;
467 | if (dst[2] != 'x'.charCodeAt())
468 | return;
469 | if (dst[3] != 's'.charCodeAt())
470 | return;
471 | let c = dst[4];
472 | if (c == '.'.charCodeAt())
473 | flag = true;
474 | else if (c == '-'.charCodeAt())
475 | flag = false;
476 | else if (c == '#'.charCodeAt()) {
477 | flag = true;
478 | binary = true;
479 | }
480 | else
481 | return;
482 | for (let i = 0; i < 8; i++) {
483 | c = dst[5 + i]
484 | if (('0'.charCodeAt() <= c) && (c <= '9'.charCodeAt()))
485 | value = (value * 16) + (c - '0'.charCodeAt());
486 | else if (('a'.charCodeAt() <= c) && (c <= 'f'.charCodeAt()))
487 | value = (value * 16) + (10 + c - 'a'.charCodeAt());
488 | else if (('A'.charCodeAt() <= c) && (c <= 'F'.charCodeAt()))
489 | value = (value * 16) + (10 + c - 'A'.charCodeAt());
490 | else
491 | return;
492 | }
493 | if (dst[13] != '?'.charCodeAt())
494 | return;
495 | if (dst[14] != '>'.charCodeAt())
496 | return;
497 | return {value: value.toString(16).padStart(8, "0"), flag, binary};
498 | }
499 | }
500 |
501 | class XsbugMessage {
502 | constructor(xml) {
503 | xml = xml.documentElement;
504 | if ("xsbug" !== xml.nodeName)
505 | throw new Error("not xsbug xml");
506 | for (let node = xml.firstChild; node; node = node.nextSibling) {
507 | XsbugMessage[node.nodeName](this, node);
508 | }
509 | return;
510 | }
511 |
512 | // node parsers
513 | static login(message, node) {
514 | message.login = {
515 | name: node.attributes.name.value,
516 | value: node.attributes.value.value,
517 | };
518 | }
519 | static samples(message, node) {
520 | message.samples = node.textContent.split(",").map(value => parseInt(value));
521 | }
522 | static frames(message, node) {
523 | message.frames = [];
524 | for (node = node.firstChild; node; node = node.nextSibling)
525 | message.frames.push(XsbugMessage.oneFrame(node));
526 | }
527 | static local(message, node) {
528 | const local = XsbugMessage.oneFrame(node);
529 | local.properties = [];
530 | for (node = node.firstChild; node; node = node.nextSibling)
531 | local.properties.push(XsbugMessage.oneProperty(node));
532 | message.local = local;
533 | }
534 | static global(message, node) {
535 | message.global = [];
536 | for (node = node.firstChild; node; node = node.nextSibling)
537 | message.global.push(XsbugMessage.oneProperty(node));
538 | message.global.sort((a, b) => a.name.localeCompare(b.name));
539 | }
540 | static grammar(message, node) {
541 | message.module = [];
542 | for (node = node.firstChild; node; node = node.nextSibling)
543 | message.module.push(XsbugMessage.oneProperty(node));
544 |
545 | message.module.sort((a, b) => a.name.localeCompare(b.name));
546 | }
547 | static break(message, node) {
548 | message.break = {
549 | message: node.textContent,
550 | };
551 | if (node.attributes.path)
552 | message.path = node.attributes.path.value;
553 | if (node.attributes.line)
554 | message.path = node.attributes.line.value;
555 | }
556 | static log(message, node) {
557 | message.log = node.textContent;
558 | }
559 | static instruments(message, node) {
560 | message.instruments = [];
561 | for (node = node.firstChild; node; node = node.nextSibling) {
562 | message.instruments.push({
563 | name: node.attributes.name.value,
564 | value: node.attributes.value.value,
565 | });
566 | }
567 | }
568 |
569 | // helpers
570 | static oneFrame(node) {
571 | const frame = {
572 | name: node.attributes.name.value,
573 | value: node.attributes.value.value,
574 | };
575 | if (node.attributes.path) {
576 | frame.path = node.attributes.path.value;
577 | frame.line = parseInt(node.attributes.line.value);
578 | }
579 | return frame;
580 | }
581 | static oneProperty(node) {
582 | const flags = node.attributes.flags.value;
583 | const property = {
584 | name: node.attributes.name.value,
585 | flags: {
586 | value: flags,
587 | delete: flags.indexOf("C") < 0,
588 | enum: flags.indexOf("E") < 0,
589 | set: flags.indexOf("W") < 0,
590 | },
591 | };
592 | if (node.attributes.value)
593 | property.value = node.attributes.value.value;
594 |
595 | if (node.firstChild) {
596 | property.property = [];
597 | for (let p = node.firstChild; p; p = p.nextSibling)
598 | property.property.push(XsbugMessage.oneProperty(p))
599 | property.property.sort((a, b) => a.name.localeCompare(b.name));
600 | }
601 |
602 | return property;
603 | }
604 | }
605 |
606 | const httpGetXSA = Uint8Array.of(0x00, 0x00, 0x01, 0xF2, 0x58, 0x53, 0x5F, 0x41, 0x00, 0x00, 0x00, 0x0C, 0x56, 0x45, 0x52, 0x53, 0x08, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x53, 0x49, 0x47, 0x4E, 0x26, 0x3B, 0x7F, 0x80, 0x47, 0x45, 0x8C, 0x6A, 0x09, 0x84, 0x18, 0x27, 0x43, 0x30, 0x81, 0x75, 0x00, 0x00, 0x00, 0x18, 0x43, 0x48, 0x4B, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87, 0x53, 0x59, 0x4D, 0x42, 0x0C, 0x00, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x00, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x00, 0x53, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x00, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x00, 0x65, 0x74, 0x63, 0x00, 0x76, 0x61, 0x6C, 0x75, 0x65, 0x00, 0x74, 0x72, 0x61, 0x63, 0x65, 0x00, 0x70, 0x61, 0x74, 0x68, 0x00, 0x68, 0x6F, 0x73, 0x74, 0x00, 0x63, 0x61, 0x6C, 0x6C, 0x62, 0x61, 0x63, 0x6B, 0x00, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x00, 0x2F, 0x55, 0x73, 0x65, 0x72, 0x73, 0x2F, 0x68, 0x6F, 0x64, 0x64, 0x69, 0x65, 0x2F, 0x50, 0x72, 0x6F, 0x6A, 0x65, 0x63, 0x74, 0x73, 0x2F, 0x72, 0x75, 0x6E, 0x6D, 0x6F, 0x64, 0x2F, 0x6D, 0x6F, 0x64, 0x73, 0x2F, 0x68, 0x74, 0x74, 0x70, 0x67, 0x65, 0x74, 0x2F, 0x6D, 0x6F, 0x64, 0x2E, 0x6A, 0x73, 0x00, 0x00, 0x00, 0x01, 0x1F, 0x4D, 0x4F, 0x44, 0x53, 0x00, 0x00, 0x00, 0x10, 0x50, 0x41, 0x54, 0x48, 0x6D, 0x6F, 0x64, 0x2E, 0x78, 0x73, 0x62, 0x00, 0x00, 0x00, 0x01, 0x07, 0x43, 0x4F, 0x44, 0x45, 0x4A, 0x0B, 0x80, 0x6B, 0x0F, 0x00, 0x4E, 0x0B, 0x80, 0x2C, 0xD1, 0x00, 0x0C, 0x00, 0x89, 0x03, 0x91, 0x02, 0x4A, 0x0B, 0x80, 0x6B, 0x11, 0x00, 0xAB, 0x15, 0x6D, 0x61, 0x6B, 0x69, 0x6E, 0x67, 0x20, 0x68, 0x74, 0x74, 0x70, 0x20, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x0A, 0x00, 0x60, 0x01, 0xBC, 0x80, 0x06, 0x80, 0x59, 0x06, 0x80, 0x21, 0x7E, 0x6B, 0x13, 0x00, 0x77, 0x7C, 0x98, 0x04, 0x52, 0x04, 0xB0, 0x08, 0x80, 0x09, 0xAB, 0x10, 0x77, 0x77, 0x77, 0x2E, 0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x00, 0x76, 0x00, 0x52, 0x04, 0xB0, 0x07, 0x80, 0x09, 0xAB, 0x02, 0x2F, 0x00, 0x76, 0x00, 0x52, 0x04, 0xB0, 0x03, 0x80, 0x09, 0x80, 0x02, 0x80, 0x59, 0x02, 0x80, 0x76, 0x00, 0xBE, 0x01, 0x60, 0x01, 0x50, 0x02, 0x73, 0x67, 0x03, 0x7E, 0x6B, 0x14, 0x00, 0x50, 0x03, 0x32, 0xFF, 0xFF, 0x2B, 0x4F, 0x0C, 0x03, 0x89, 0x03, 0x75, 0x0A, 0x80, 0x75, 0x05, 0x80, 0x75, 0x04, 0x80, 0x4A, 0x0B, 0x80, 0x6B, 0x14, 0x00, 0x02, 0x00, 0xC2, 0x02, 0x7E, 0x02, 0x01, 0xC2, 0x03, 0x7E, 0x02, 0x02, 0xC2, 0x04, 0x7E, 0x6B, 0x16, 0x00, 0x60, 0x05, 0x52, 0x02, 0x42, 0x18, 0x22, 0x6B, 0x17, 0x00, 0x52, 0x03, 0x60, 0x01, 0xBC, 0x80, 0x06, 0x80, 0x59, 0x06, 0x80, 0x21, 0x7E, 0x6B, 0x18, 0x00, 0xAB, 0x02, 0x0A, 0x00, 0x60, 0x01, 0xBC, 0x80, 0x06, 0x80, 0x59, 0x06, 0x80, 0x21, 0x7E, 0x3D, 0x9A, 0x09, 0x80, 0x7E, 0x3D, 0x41, 0x7E, 0xB0, 0x00, 0x80, 0x6B, 0x0F, 0x00, 0xAB, 0x05, 0x68, 0x74, 0x74, 0x70, 0x00, 0xB0, 0x00, 0x80, 0x60, 0x03, 0xB8, 0xB0, 0x01, 0x80, 0x7A, 0x7A, 0x60, 0x03, 0xB8, 0x60, 0x03, 0x6D, 0x8F, 0x3D, 0x00, 0x00, 0x00, 0x08, 0x52, 0x53, 0x52, 0x43).buffer;
607 | const helloWorldXSA = Uint8Array.of(0x00, 0x00, 0x01, 0xA3, 0x58, 0x53, 0x5F, 0x41, 0x00, 0x00, 0x00, 0x0C, 0x56, 0x45, 0x52, 0x53, 0x08, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x53, 0x49, 0x47, 0x4E, 0xFC, 0x65, 0x34, 0x94, 0x71, 0x9B, 0xE2, 0x03, 0x00, 0xC7, 0x02, 0x39, 0x05, 0xC6, 0xC1, 0x9B, 0x00, 0x00, 0x00, 0x18, 0x43, 0x48, 0x4B, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0x53, 0x59, 0x4D, 0x42, 0x06, 0x00, 0x78, 0x00, 0x2F, 0x6D, 0x63, 0x2F, 0x6D, 0x6F, 0x64, 0x2E, 0x6A, 0x73, 0x00, 0x74, 0x72, 0x61, 0x63, 0x65, 0x00, 0x63, 0x6F, 0x6E, 0x66, 0x69, 0x67, 0x00, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, 0x00, 0x2F, 0x6D, 0x6F, 0x64, 0x64, 0x61, 0x62, 0x6C, 0x65, 0x2F, 0x62, 0x75, 0x69, 0x6C, 0x64, 0x2F, 0x74, 0x6D, 0x70, 0x2F, 0x77, 0x61, 0x73, 0x6D, 0x2F, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2F, 0x6D, 0x63, 0x2F, 0x63, 0x68, 0x65, 0x63, 0x6B, 0x2E, 0x6A, 0x73, 0x00, 0x00, 0x00, 0x01, 0x00, 0x4D, 0x4F, 0x44, 0x53, 0x00, 0x00, 0x00, 0x10, 0x50, 0x41, 0x54, 0x48, 0x6D, 0x6F, 0x64, 0x2E, 0x78, 0x73, 0x62, 0x00, 0x00, 0x00, 0x00, 0x76, 0x43, 0x4F, 0x44, 0x45, 0x4A, 0x01, 0x80, 0x6B, 0x0F, 0x00, 0x4E, 0x01, 0x80, 0x2B, 0x5C, 0x0C, 0x00, 0x89, 0x02, 0x4A, 0x01, 0x80, 0x6B, 0x0F, 0x00, 0xAB, 0x0D, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x0A, 0x00, 0x60, 0x01, 0xBC, 0x80, 0x02, 0x80, 0x59, 0x02, 0x80, 0x21, 0x7E, 0x6B, 0x11, 0x00, 0x75, 0x00, 0x80, 0x60, 0x00, 0x69, 0x02, 0x7E, 0x87, 0x02, 0x52, 0x02, 0x60, 0x0A, 0x65, 0x18, 0x21, 0x6B, 0x12, 0x00, 0x52, 0x02, 0xAB, 0x02, 0x0A, 0x00, 0x60, 0x02, 0xBC, 0x80, 0x02, 0x80, 0x59, 0x02, 0x80, 0x21, 0x7E, 0x87, 0x02, 0x6B, 0x11, 0x00, 0x52, 0x02, 0x5D, 0x98, 0x02, 0x7E, 0x15, 0xD8, 0xBE, 0x01, 0x3D, 0x41, 0x7E, 0x60, 0x01, 0x6D, 0x8F, 0x3D, 0x00, 0x00, 0x00, 0x12, 0x50, 0x41, 0x54, 0x48, 0x63, 0x68, 0x65, 0x63, 0x6B, 0x2E, 0x78, 0x73, 0x62, 0x00, 0x00, 0x00, 0x00, 0x60, 0x43, 0x4F, 0x44, 0x45, 0x4A, 0x05, 0x80, 0x6B, 0x03, 0x00, 0x4E, 0x05, 0x80, 0x2B, 0x23, 0x0C, 0x00, 0x89, 0x02, 0x91, 0x02, 0x4A, 0x05, 0x80, 0x6B, 0x05, 0x00, 0x32, 0x04, 0x80, 0x2B, 0x0B, 0x0C, 0x00, 0x4A, 0x05, 0x80, 0x6B, 0x06, 0x00, 0xBC, 0x7E, 0x3D, 0xC0, 0x03, 0x7E, 0x6B, 0x05, 0x00, 0x3D, 0x41, 0x7E, 0xB0, 0x03, 0x80, 0x6B, 0x03, 0x00, 0xAB, 0x0A, 0x6D, 0x63, 0x2F, 0x63, 0x6F, 0x6E, 0x66, 0x69, 0x67, 0x00, 0xB0, 0x04, 0x80, 0x60, 0x03, 0xB8, 0xB0, 0x04, 0x80, 0x7A, 0x7A, 0xB0, 0x04, 0x80, 0x60, 0x04, 0xB8, 0x60, 0x03, 0x6D, 0x8F, 0x3D, 0x00, 0x00, 0x00, 0x08, 0x52, 0x53, 0x52, 0x43).buffer;
608 | const ballsXSA = Uint8Array.of(0x00, 0x00, 0x0A, 0xAA, 0x58, 0x53, 0x5F, 0x41, 0x00, 0x00, 0x00, 0x0C, 0x56, 0x45, 0x52, 0x53, 0x08, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x53, 0x49, 0x47, 0x4E, 0xDA, 0xC0, 0xB4, 0xFE, 0x46, 0xF4, 0x1E, 0x52, 0x40, 0xBA, 0x31, 0x96, 0x4F, 0x15, 0xB5, 0x8B, 0x00, 0x00, 0x00, 0x18, 0x43, 0x48, 0x4B, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x99, 0x53, 0x59, 0x4D, 0x42, 0x2E, 0x00, 0x24, 0x00, 0x78, 0x00, 0x79, 0x00, 0x42, 0x61, 0x6C, 0x6C, 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6F, 0x72, 0x00, 0x41, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x00, 0x74, 0x65, 0x6D, 0x70, 0x6C, 0x61, 0x74, 0x65, 0x00, 0x62, 0x61, 0x63, 0x6B, 0x67, 0x72, 0x6F, 0x75, 0x6E, 0x64, 0x53, 0x6B, 0x69, 0x6E, 0x00, 0x54, 0x65, 0x78, 0x74, 0x75, 0x72, 0x65, 0x00, 0x64, 0x78, 0x00, 0x64, 0x79, 0x00, 0x74, 0x65, 0x78, 0x74, 0x75, 0x72, 0x65, 0x00, 0x6F, 0x6E, 0x54, 0x69, 0x6D, 0x65, 0x43, 0x68, 0x61, 0x6E, 0x67, 0x65, 0x64, 0x00, 0x64, 0x69, 0x73, 0x70, 0x6C, 0x61, 0x79, 0x4C, 0x69, 0x73, 0x74, 0x4C, 0x65, 0x6E, 0x67, 0x74, 0x68, 0x00, 0x62, 0x61, 0x6C, 0x6C, 0x54, 0x65, 0x78, 0x74, 0x75, 0x72, 0x65, 0x00, 0x2F, 0x6D, 0x63, 0x2F, 0x6D, 0x6F, 0x64, 0x2E, 0x6A, 0x73, 0x00, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x00, 0x42, 0x61, 0x6C, 0x6C, 0x41, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x00, 0x62, 0x6F, 0x74, 0x74, 0x6F, 0x6D, 0x00, 0x6C, 0x65, 0x6E, 0x67, 0x74, 0x68, 0x00, 0x74, 0x6F, 0x70, 0x00, 0x6F, 0x6E, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x00, 0x63, 0x6F, 0x6E, 0x74, 0x65, 0x6E, 0x74, 0x73, 0x00, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, 0x00, 0x6D, 0x6F, 0x76, 0x65, 0x42, 0x79, 0x00, 0x64, 0x65, 0x6C, 0x74, 0x61, 0x00, 0x63, 0x6F, 0x6C, 0x6F, 0x72, 0x00, 0x43, 0x6F, 0x6E, 0x74, 0x65, 0x6E, 0x74, 0x00, 0x72, 0x69, 0x67, 0x68, 0x74, 0x00, 0x53, 0x6B, 0x69, 0x6E, 0x00, 0x62, 0x61, 0x6C, 0x6C, 0x53, 0x6B, 0x69, 0x6E, 0x00, 0x77, 0x69, 0x64, 0x74, 0x68, 0x00, 0x73, 0x74, 0x61, 0x74, 0x65, 0x00, 0x62, 0x61, 0x6C, 0x6C, 0x00, 0x73, 0x74, 0x61, 0x72, 0x74, 0x00, 0x66, 0x69, 0x6C, 0x6C, 0x00, 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6F, 0x72, 0x00, 0x6C, 0x65, 0x66, 0x74, 0x00, 0x74, 0x6F, 0x75, 0x63, 0x68, 0x43, 0x6F, 0x75, 0x6E, 0x74, 0x00, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x65, 0x72, 0x00, 0x73, 0x6B, 0x69, 0x6E, 0x00, 0x6F, 0x6E, 0x44, 0x69, 0x73, 0x70, 0x6C, 0x61, 0x79, 0x69, 0x6E, 0x67, 0x00, 0x63, 0x6F, 0x6E, 0x66, 0x69, 0x67, 0x00, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x00, 0x72, 0x6F, 0x74, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x00, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x00, 0x2F, 0x6D, 0x6F, 0x64, 0x64, 0x61, 0x62, 0x6C, 0x65, 0x2F, 0x62, 0x75, 0x69, 0x6C, 0x64, 0x2F, 0x74, 0x6D, 0x70, 0x2F, 0x77, 0x61, 0x73, 0x6D, 0x2F, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2F, 0x6D, 0x63, 0x2F, 0x63, 0x68, 0x65, 0x63, 0x6B, 0x2E, 0x6A, 0x73, 0x00, 0x00, 0x00, 0x06, 0x2E, 0x4D, 0x4F, 0x44, 0x53, 0x00, 0x00, 0x00, 0x10, 0x50, 0x41, 0x54, 0x48, 0x6D, 0x6F, 0x64, 0x2E, 0x78, 0x73, 0x62, 0x00, 0x00, 0x00, 0x04, 0xE6, 0x43, 0x4F, 0x44, 0x45, 0x4A, 0x0E, 0x80, 0x6B, 0x0F, 0x00, 0x4E, 0x0E, 0x80, 0x2C, 0x8A, 0x04, 0x0C, 0x00, 0x89, 0x0A, 0x91, 0x06, 0x4A, 0x0E, 0x80, 0x6B, 0x11, 0x00, 0x77, 0x7C, 0x98, 0x08, 0x52, 0x08, 0xB0, 0x22, 0x80, 0x09, 0xAB, 0x07, 0x73, 0x69, 0x6C, 0x76, 0x65, 0x72, 0x00, 0x76, 0x00, 0xBE, 0x01, 0x60, 0x01, 0x80, 0x1C, 0x80, 0x59, 0x1C, 0x80, 0x73, 0x2E, 0x02, 0x7E, 0x6B, 0x12, 0x00, 0xAB, 0x08, 0x6D, 0x6F, 0x64, 0x2E, 0x70, 0x6E, 0x67, 0x00, 0x60, 0x01, 0x80, 0x07, 0x80, 0x59, 0x07, 0x80, 0x73, 0x2E, 0x03, 0x7E, 0x6B, 0x13, 0x00, 0x77, 0x7C, 0x98, 0x08, 0x52, 0x08, 0xB0, 0x0A, 0x80, 0x09, 0x50, 0x03, 0x76, 0x00, 0x52, 0x08, 0xB0, 0x19, 0x80, 0x09, 0x77, 0x06, 0x98, 0x09, 0x52, 0x09, 0x60, 0x04, 0x9A, 0x12, 0x80, 0x7E, 0x60, 0x00, 0x52, 0x09, 0x3B, 0x54, 0x22, 0x80, 0x21, 0x7E, 0x52, 0x09, 0x60, 0x00, 0x09, 0xAB, 0x04, 0x72, 0x65, 0x64, 0x00, 0x9B, 0x7E, 0x52, 0x09, 0x60, 0x01, 0x09, 0xAB, 0x06, 0x67, 0x72, 0x65, 0x65, 0x6E, 0x00, 0x9B, 0x7E, 0x52, 0x09, 0x60, 0x02, 0x09, 0xAB, 0x05, 0x62, 0x6C, 0x75, 0x65, 0x00, 0x9B, 0x7E, 0x52, 0x09, 0x60, 0x03, 0x09, 0xAB, 0x05, 0x67, 0x72, 0x61, 0x79, 0x00, 0x9B, 0x7E, 0xBE, 0x01, 0x76, 0x00, 0x52, 0x08, 0xB0, 0x01, 0x80, 0x09, 0x60, 0x00, 0x76, 0x00, 0x52, 0x08, 0xB0, 0x02, 0x80, 0x09, 0x60, 0x00, 0x76, 0x00, 0x52, 0x08, 0xB0, 0x1E, 0x80, 0x09, 0x60, 0x20, 0x76, 0x00, 0x52, 0x08, 0xB0, 0x0F, 0x80, 0x09, 0x60, 0x20, 0x76, 0x00, 0xBE, 0x01, 0x60, 0x01, 0x80, 0x1C, 0x80, 0x59, 0x1C, 0x80, 0x73, 0x2E, 0x04, 0x7E, 0x6B, 0x15, 0x00, 0x77, 0x77, 0x74, 0x03, 0x80, 0x80, 0x23, 0x80, 0x59, 0x23, 0x80, 0x48, 0x98, 0x08, 0x6B, 0x16, 0x00, 0x32, 0xFF, 0xFF, 0x2B, 0x0E, 0x0E, 0x00, 0x4A, 0x0E, 0x80, 0x6B, 0x17, 0x00, 0x60, 0x00, 0xAE, 0x9E, 0x7E, 0x40, 0xB6, 0x98, 0x09, 0x27, 0x52, 0x09, 0x72, 0x03, 0x80, 0x52, 0x08, 0xB0, 0x14, 0x80, 0x09, 0x6B, 0x19, 0x00, 0x4E, 0xFF, 0xFF, 0x2B, 0x2F, 0x0C, 0x02, 0x89, 0x02, 0x75, 0x20, 0x80, 0x75, 0x18, 0x80, 0x4A, 0x0E, 0x80, 0x6B, 0x19, 0x00, 0x02, 0x00, 0xC2, 0x02, 0x7E, 0x02, 0x01, 0xC2, 0x03, 0x7E, 0x6B, 0x1A, 0x00, 0xB3, 0x52, 0x03, 0x9A, 0x08, 0x80, 0x7E, 0x6B, 0x1B, 0x00, 0xB3, 0x52, 0x03, 0x9A, 0x09, 0x80, 0x7E, 0x3D, 0x76, 0x14, 0x52, 0x08, 0xB0, 0x28, 0x80, 0x09, 0x6B, 0x1D, 0x00, 0x4E, 0xFF, 0xFF, 0x2B, 0x66, 0x0C, 0x01, 0x89, 0x01, 0x75, 0x20, 0x80, 0x4A, 0x0E, 0x80, 0x6B, 0x1D, 0x00, 0x02, 0x00, 0xC2, 0x02, 0x7E, 0x6B, 0x1E, 0x00, 0xB3, 0x52, 0x02, 0x54, 0x01, 0x80, 0x9A, 0x01, 0x80, 0x7E, 0x6B, 0x1F, 0x00, 0xB3, 0x52, 0x02, 0x54, 0x02, 0x80, 0x9A, 0x02, 0x80, 0x7E, 0x6B, 0x20, 0x00, 0xB3, 0x52, 0x02, 0x54, 0x26, 0x80, 0x54, 0x1E, 0x80, 0x52, 0x02, 0x54, 0x1E, 0x80, 0xAD, 0x9A, 0x1E, 0x80, 0x7E, 0x6B, 0x21, 0x00, 0xB3, 0x52, 0x02, 0x54, 0x26, 0x80, 0x54, 0x0F, 0x80, 0x52, 0x02, 0x54, 0x0F, 0x80, 0xAD, 0x9A, 0x0F, 0x80, 0x7E, 0x6B, 0x22, 0x00, 0x60, 0x00, 0x52, 0x02, 0x3B, 0x54, 0x21, 0x80, 0x21, 0x7E, 0x3D, 0x76, 0x14, 0x52, 0x08, 0xB0, 0x0B, 0x80, 0x09, 0x6B, 0x24, 0x00, 0x4E, 0xFF, 0xFF, 0x2C, 0xCC, 0x00, 0x0C, 0x01, 0x89, 0x05, 0x75, 0x20, 0x80, 0x4A, 0x0E, 0x80, 0x6B, 0x24, 0x00, 0x02, 0x00, 0xC2, 0x02, 0x7E, 0x75, 0x08, 0x80, 0xBC, 0xC2, 0x03, 0x7E, 0x75, 0x09, 0x80, 0xBC, 0xC2, 0x04, 0x7E, 0x75, 0x01, 0x80, 0xBC, 0xC2, 0x05, 0x7E, 0x75, 0x02, 0x80, 0xBC, 0xC2, 0x06, 0x7E, 0x6B, 0x25, 0x00, 0xB3, 0x54, 0x08, 0x80, 0xC2, 0x03, 0x7E, 0x6B, 0x26, 0x00, 0xB3, 0x54, 0x09, 0x80, 0xC2, 0x04, 0x7E, 0x6B, 0x27, 0x00, 0x52, 0x03, 0x52, 0x04, 0x60, 0x02, 0x52, 0x02, 0x3B, 0x54, 0x17, 0x80, 0x21, 0x7E, 0x6B, 0x28, 0x00, 0xB3, 0x54, 0x01, 0x80, 0x52, 0x03, 0x01, 0xC2, 0x05, 0x7E, 0x6B, 0x29, 0x00, 0xB3, 0x54, 0x02, 0x80, 0x52, 0x04, 0x01, 0xC2, 0x06, 0x7E, 0x6B, 0x2A, 0x00, 0x52, 0x05, 0x60, 0x00, 0x65, 0x3B, 0x1B, 0x08, 0x7E, 0x52, 0x05, 0xB3, 0x54, 0x1E, 0x80, 0x6F, 0x18, 0x05, 0x52, 0x03, 0x6C, 0x83, 0x03, 0x6B, 0x2B, 0x00, 0x52, 0x06, 0x60, 0x00, 0x65, 0x3B, 0x1B, 0x08, 0x7E, 0x52, 0x06, 0xB3, 0x54, 0x0F, 0x80, 0x6F, 0x18, 0x05, 0x52, 0x04, 0x6C, 0x83, 0x04, 0x6B, 0x2C, 0x00, 0xB3, 0x52, 0x03, 0x9A, 0x08, 0x80, 0x7E, 0x6B, 0x2D, 0x00, 0xB3, 0x52, 0x04, 0x9A, 0x09, 0x80, 0x7E, 0x6B, 0x2E, 0x00, 0xB3, 0x52, 0x05, 0x9A, 0x01, 0x80, 0x7E, 0x6B, 0x2F, 0x00, 0xB3, 0x52, 0x06, 0x9A, 0x02, 0x80, 0x7E, 0xBE, 0x04, 0x3D, 0x76, 0x14, 0x2E, 0x0A, 0xBE, 0x01, 0xBE, 0x02, 0x6B, 0x15, 0x00, 0x67, 0x05, 0x7E, 0x6B, 0x33, 0x00, 0x4E, 0xFF, 0xFF, 0x2C, 0x8B, 0x01, 0x0C, 0x01, 0x89, 0x07, 0x91, 0x03, 0x93, 0x94, 0x75, 0x00, 0x80, 0x4A, 0x0E, 0x80, 0x6B, 0x33, 0x00, 0x02, 0x00, 0xC2, 0x05, 0x7E, 0x6B, 0x33, 0x00, 0x77, 0x7C, 0x98, 0x06, 0x52, 0x06, 0xB0, 0x27, 0x80, 0x09, 0x6B, 0x34, 0x00, 0x50, 0x02, 0x76, 0x00, 0x52, 0x06, 0xB0, 0x15, 0x80, 0x09, 0x6B, 0x35, 0x00, 0x77, 0x06, 0x98, 0x07, 0x52, 0x07, 0x60, 0x04, 0x9A, 0x12, 0x80, 0x7E, 0x60, 0x00, 0x52, 0x07, 0x3B, 0x54, 0x22, 0x80, 0x21, 0x7E, 0x52, 0x07, 0x60, 0x00, 0x09, 0x6B, 0x36, 0x00, 0x60, 0x06, 0x77, 0x7C, 0x98, 0x08, 0x52, 0x08, 0xB0, 0x24, 0x80, 0x09, 0x60, 0x00, 0x76, 0x00, 0x52, 0x08, 0xB0, 0x13, 0x80, 0x09, 0x60, 0x00, 0x76, 0x00, 0x52, 0x08, 0xB0, 0x27, 0x80, 0x09, 0x50, 0x03, 0x76, 0x00, 0x52, 0x08, 0xB0, 0x1F, 0x80, 0x09, 0x60, 0x00, 0x76, 0x00, 0x52, 0x08, 0xB0, 0x23, 0x80, 0x09, 0x50, 0x04, 0x76, 0x00, 0xBE, 0x01, 0x60, 0x02, 0xBC, 0x80, 0x1A, 0x80, 0x59, 0x1A, 0x80, 0x21, 0x9B, 0x7E, 0x52, 0x07, 0x60, 0x01, 0x09, 0x6B, 0x37, 0x00, 0x60, 0x05, 0x77, 0x7C, 0x98, 0x08, 0x52, 0x08, 0xB0, 0x1B, 0x80, 0x09, 0x60, 0x00, 0x76, 0x00, 0x52, 0x08, 0xB0, 0x13, 0x80, 0x09, 0x60, 0x00, 0x76, 0x00, 0x52, 0x08, 0xB0, 0x27, 0x80, 0x09, 0x50, 0x03, 0x76, 0x00, 0x52, 0x08, 0xB0, 0x1F, 0x80, 0x09, 0x60, 0x01, 0x76, 0x00, 0x52, 0x08, 0xB0, 0x23, 0x80, 0x09, 0x50, 0x04, 0x76, 0x00, 0xBE, 0x01, 0x60, 0x02, 0xBC, 0x80, 0x1A, 0x80, 0x59, 0x1A, 0x80, 0x21, 0x9B, 0x7E, 0x52, 0x07, 0x60, 0x02, 0x09, 0x6B, 0x38, 0x00, 0x60, 0x04, 0x77, 0x7C, 0x98, 0x08, 0x52, 0x08, 0xB0, 0x1B, 0x80, 0x09, 0x60, 0x00, 0x76, 0x00, 0x52, 0x08, 0xB0, 0x11, 0x80, 0x09, 0x60, 0x00, 0x76, 0x00, 0x52, 0x08, 0xB0, 0x27, 0x80, 0x09, 0x50, 0x03, 0x76, 0x00, 0x52, 0x08, 0xB0, 0x1F, 0x80, 0x09, 0x60, 0x02, 0x76, 0x00, 0x52, 0x08, 0xB0, 0x23, 0x80, 0x09, 0x50, 0x04, 0x76, 0x00, 0xBE, 0x01, 0x60, 0x02, 0xBC, 0x80, 0x1A, 0x80, 0x59, 0x1A, 0x80, 0x21, 0x9B, 0x7E, 0x52, 0x07, 0x60, 0x03, 0x09, 0x6B, 0x39, 0x00, 0x60, 0x03, 0x77, 0x7C, 0x98, 0x08, 0x52, 0x08, 0xB0, 0x24, 0x80, 0x09, 0x60, 0x00, 0x76, 0x00, 0x52, 0x08, 0xB0, 0x11, 0x80, 0x09, 0x60, 0x00, 0x76, 0x00, 0x52, 0x08, 0xB0, 0x27, 0x80, 0x09, 0x50, 0x03, 0x76, 0x00, 0x52, 0x08, 0xB0, 0x1F, 0x80, 0x09, 0x60, 0x03, 0x76, 0x00, 0x52, 0x08, 0xB0, 0x23, 0x80, 0x09, 0x50, 0x04, 0x76, 0x00, 0xBE, 0x01, 0x60, 0x02, 0xBC, 0x80, 0x1A, 0x80, 0x59, 0x1A, 0x80, 0x21, 0x9B, 0x7E, 0xBE, 0x01, 0x76, 0x00, 0xBE, 0x01, 0x8F, 0x15, 0x00, 0x3E, 0x41, 0xA4, 0x02, 0xA4, 0x04, 0xA4, 0x05, 0xA6, 0x7E, 0x60, 0x01, 0x80, 0x04, 0x80, 0x59, 0x04, 0x80, 0x3B, 0x54, 0x05, 0x80, 0x21, 0x67, 0x06, 0x7E, 0x6B, 0x3D, 0x00, 0x7A, 0x77, 0x7C, 0x98, 0x08, 0x52, 0x08, 0xB0, 0x0C, 0x80, 0x09, 0x61, 0x00, 0x10, 0x76, 0x00, 0x52, 0x08, 0xB0, 0x25, 0x80, 0x09, 0x60, 0x00, 0x76, 0x00, 0xBE, 0x01, 0x60, 0x02, 0x50, 0x06, 0x73, 0x67, 0x07, 0x7E, 0x3D, 0x41, 0x7E, 0x7A, 0xAB, 0x07, 0x70, 0x69, 0x75, 0x2F, 0x4D, 0x43, 0x00, 0x7A, 0x60, 0x03, 0xB8, 0xB0, 0x06, 0x80, 0x7A, 0x7A, 0x60, 0x03, 0xB8, 0xB0, 0x0D, 0x80, 0x7A, 0x7A, 0x60, 0x03, 0xB8, 0xB0, 0x1D, 0x80, 0x7A, 0x7A, 0x60, 0x03, 0xB8, 0xB0, 0x03, 0x80, 0x7A, 0x7A, 0x60, 0x03, 0xB8, 0xB0, 0x10, 0x80, 0x7A, 0x7A, 0x60, 0x03, 0xB8, 0xB0, 0x16, 0x80, 0x7A, 0x7A, 0xB0, 0x16, 0x80, 0x60, 0x04, 0xB8, 0x60, 0x08, 0x6D, 0x8F, 0x3D, 0x00, 0x00, 0x00, 0x12, 0x50, 0x41, 0x54, 0x48, 0x63, 0x68, 0x65, 0x63, 0x6B, 0x2E, 0x78, 0x73, 0x62, 0x00, 0x00, 0x00, 0x01, 0x1E, 0x43, 0x4F, 0x44, 0x45, 0x4A, 0x2D, 0x80, 0x6B, 0x03, 0x00, 0x4E, 0x2D, 0x80, 0x2C, 0xE0, 0x00, 0x0C, 0x00, 0x89, 0x02, 0x91, 0x02, 0x4A, 0x2D, 0x80, 0x6B, 0x05, 0x00, 0x32, 0x16, 0x80, 0x2C, 0xC3, 0x00, 0x0C, 0x00, 0x89, 0x01, 0x91, 0x01, 0x4A, 0x2D, 0x80, 0x6B, 0x06, 0x00, 0x50, 0x02, 0x54, 0x2A, 0x80, 0xAB, 0x09, 0x52, 0x47, 0x42, 0x35, 0x36, 0x35, 0x4C, 0x45, 0x00, 0x79, 0x18, 0x50, 0x6B, 0x07, 0x00, 0xAB, 0x23, 0x69, 0x6E, 0x63, 0x6F, 0x6D, 0x70, 0x61, 0x74, 0x69, 0x62, 0x6C, 0x65, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x3A, 0x20, 0x70, 0x69, 0x78, 0x65, 0x6C, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x3A, 0x20, 0x00, 0x50, 0x02, 0x54, 0x2A, 0x80, 0x01, 0xAB, 0x15, 0x20, 0x69, 0x6E, 0x73, 0x74, 0x65, 0x61, 0x64, 0x20, 0x6F, 0x66, 0x20, 0x52, 0x47, 0x42, 0x35, 0x36, 0x35, 0x4C, 0x45, 0x00, 0x01, 0x60, 0x01, 0x80, 0x2C, 0x80, 0x59, 0x2C, 0x80, 0x73, 0xB4, 0x6B, 0x08, 0x00, 0x50, 0x02, 0x54, 0x2B, 0x80, 0x60, 0x00, 0x79, 0x18, 0x46, 0x6B, 0x09, 0x00, 0xAB, 0x20, 0x69, 0x6E, 0x63, 0x6F, 0x6D, 0x70, 0x61, 0x74, 0x69, 0x62, 0x6C, 0x65, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x3A, 0x20, 0x72, 0x6F, 0x74, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x3A, 0x20, 0x00, 0x50, 0x02, 0x54, 0x2B, 0x80, 0x01, 0xAB, 0x0E, 0x20, 0x69, 0x6E, 0x73, 0x74, 0x65, 0x61, 0x64, 0x20, 0x6F, 0x66, 0x20, 0x30, 0x00, 0x01, 0x60, 0x01, 0x80, 0x2C, 0x80, 0x59, 0x2C, 0x80, 0x73, 0xB4, 0x3D, 0x41, 0xA4, 0x02, 0x7E, 0xC0, 0x03, 0x7E, 0x6B, 0x05, 0x00, 0x3D, 0x41, 0x7E, 0xB0, 0x29, 0x80, 0x6B, 0x03, 0x00, 0xAB, 0x0A, 0x6D, 0x63, 0x2F, 0x63, 0x6F, 0x6E, 0x66, 0x69, 0x67, 0x00, 0xB0, 0x16, 0x80, 0x60, 0x03, 0xB8, 0xB0, 0x16, 0x80, 0x7A, 0x7A, 0xB0, 0x16, 0x80, 0x60, 0x04, 0xB8, 0x60, 0x03, 0x6D, 0x8F, 0x3D, 0x00, 0x00, 0x02, 0x9F, 0x52, 0x53, 0x52, 0x43, 0x00, 0x00, 0x00, 0x19, 0x50, 0x41, 0x54, 0x48, 0x6D, 0x6F, 0x64, 0x2D, 0x61, 0x6C, 0x70, 0x68, 0x61, 0x2E, 0x62, 0x6D, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x7E, 0x44, 0x41, 0x54, 0x41, 0x42, 0x4D, 0x76, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00, 0x00, 0x12, 0x0B, 0x00, 0x00, 0x12, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x11, 0x11, 0x00, 0x22, 0x22, 0x22, 0x00, 0x33, 0x33, 0x33, 0x00, 0x44, 0x44, 0x44, 0x00, 0x55, 0x55, 0x55, 0x00, 0x66, 0x66, 0x66, 0x00, 0x77, 0x77, 0x77, 0x00, 0x88, 0x88, 0x88, 0x00, 0x99, 0x99, 0x99, 0x00, 0xAA, 0xAA, 0xAA, 0x00, 0xBB, 0xBB, 0xBB, 0x00, 0xCC, 0xCC, 0xCC, 0x00, 0xDD, 0xDD, 0xDD, 0x00, 0xEE, 0xEE, 0xEE, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xD9, 0x63, 0x10, 0x01, 0x36, 0x9D, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xB6, 0x10, 0x00, 0x00, 0x00, 0x00, 0x01, 0x6B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xD7, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x7D, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, 0xB3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3B, 0xFF, 0xFF, 0xFF, 0xFB, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xBF, 0xFF, 0xFF, 0xD4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4D, 0xFF, 0xFF, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFB, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xBF, 0xF6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6F, 0xD1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1D, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0xD1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1D, 0xF6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6F, 0xFB, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xBF, 0xFF, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0xD4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4D, 0xFF, 0xFF, 0xFB, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xBF, 0xFF, 0xFF, 0xFF, 0xB3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xD7, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x7D, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xB6, 0x10, 0x00, 0x00, 0x00, 0x00, 0x01, 0x6B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xD9, 0x63, 0x10, 0x01, 0x36, 0x9D, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF).buffer;
609 |
610 | function onLogin(msg) {
611 | this.onBreak = onBreak;
612 | this.doSetAllBreakpoints([{path: "/Users/hoddie/Projects/moddable/examples/network/websocket/websocketserver/main.js", line: 51}]);
613 | this.doStep();
614 |
615 | this.doSetPreference("config", "when", "boot");
616 | this.doGetPreference("config", "when", function(code, data) {
617 | if (code) {
618 | console.log("can't get config/when pref");
619 | return;
620 | }
621 | let result = "";
622 | data = new Uint8Array(data);
623 | for (let i = 0; i < data.byteLength - 1; i++)
624 | result += String.fromCharCode(data[i]);
625 | console.log(`config/when pref is "${result}"`);
626 | });
627 |
628 | setTimeout(function() {
629 | console.log("START INSTALL.");
630 | xsb.onBreak = function() {}
631 | xsb.doStep();
632 | xsb.doInstall(helloWorldXSA, function() {
633 | console.log("INSTALL COMPLETE. RESTART.");
634 | xsb.doRestart();
635 | });
636 | }, 10 * 1000);
637 | }
638 | function onBreak(msg) {
639 | this.doGo();
640 | }
641 |
642 | let xsb;
643 |
644 | // document.querySelector("#usb-connect-button").addEventListener("click", () => {
645 | // if (xsb) {
646 | // xsb.disconnect();
647 | // xsb = undefined;
648 | // }
649 | // xsb = new XsbugUSB({baud: 921600 / 2});
650 | // xsb.onLogin = onLogin;
651 | // });
652 | // document.querySelector("#wifi-connect-button").addEventListener("click", () => {
653 | // if (xsb) {
654 | // xsb.disconnect();
655 | // xsb = undefined;
656 | // }
657 | // xsb = new XsbugWebSocket("ws://runmod.local:8080");
658 | // xsb.onLogin = onLogin;
659 | // });
660 |
661 | function tracePacket(prefix, bytes) {
662 | return;
663 | for (let i = 0; i < bytes.length; i += 16) {
664 | let line = prefix;
665 | let end = i + 16;
666 | if (end > bytes.length) end = bytes.length;
667 | for (let j = i; j < end; j++) {
668 | let byte = bytes[j].toString(16);
669 | if (byte.length < 2) byte = "0" + byte;
670 | line += byte + " ";
671 | }
672 | line += " ";
673 | for (let j = i; j < end; j++) {
674 | let byte = bytes[j];
675 | if ((32 <= byte) && (byte < 128))
676 | line += String.fromCharCode(byte);
677 | else
678 | line += ".";
679 | }
680 | console.log(line);
681 | }
682 | }
--------------------------------------------------------------------------------
/resources/lib/xsserial.js:
--------------------------------------------------------------------------------
1 | class XsbugMessageEx {
2 |
3 | constructor(xml) {
4 | xml = xml.documentElement;
5 | if ("xsbug" !== xml.nodeName)
6 | throw new Error("not xsbug xml");
7 | for (let node = xml.firstChild; node; node = node.nextSibling) {
8 |
9 | if (XsbugMessageEx[node.nodeName]) {
10 | XsbugMessageEx[node.nodeName](this, node);
11 | } else {
12 | XsbugMessage[node.nodeName](this, node);
13 | }
14 | }
15 | }
16 |
17 | static bubble(message, node) {
18 |
19 | message.bubble = {
20 | node: node.getAttribute('name'),
21 | data: node.textContent
22 | };
23 | }
24 |
25 | }
26 |
27 | class XsbugSerial extends XsbugConnection {
28 |
29 | #active;
30 | #reader;
31 | #readLoop;
32 |
33 | constructor(options = {}) {
34 | super();
35 | this.baud = options.baudRate || 921600;
36 | this.dst = new Uint8Array(32768);
37 | // this.connect();
38 |
39 | this.port = options.port
40 | }
41 |
42 | reset() {
43 | this.binary = false;
44 | this.dstIndex = 0;
45 | this.currentMachine = undefined;
46 | this.#active = false;
47 | }
48 |
49 | async connect(device) {
50 |
51 | // https://developer.chrome.com/articles/serial/
52 |
53 | let xs = this;
54 | await xs.closeDevice();
55 |
56 | async function readUntilClosed(port) {
57 | while (port.readable && xs.#active) {
58 | xs.#reader = port.readable.getReader();
59 | try {
60 | while (true) {
61 | const { value, done } = await xs.#reader.read();
62 | if (done) {
63 | // reader.cancel() has been called.
64 | break;
65 | }
66 | // value is a Uint8Array.
67 | // console.log(value);
68 | xs.usbReceive(value);
69 | }
70 | } catch (error) {
71 | console.log(error.toString());
72 | // Handle error...
73 | } finally {
74 | // Allow the serial port to be closed later.
75 | xs.#reader.releaseLock();
76 | }
77 | }
78 | await port.close();
79 | }
80 |
81 | try {
82 | xs.reset();
83 |
84 | if (device) {
85 | xs.port = device;
86 | }
87 |
88 | if (!xs.port) {
89 | await xs.getDevice();
90 | }
91 |
92 | await xs.openDevice();
93 |
94 | xs.#active = true;
95 | xs.#readLoop = readUntilClosed(xs.port);
96 |
97 | }
98 | catch (e) {
99 | console.log("Connect error: ", e.toString());
100 | }
101 | }
102 |
103 | async getDevice() {
104 | this.port = await navigator.serial.requestPort({});
105 | }
106 |
107 | async openDevice(baud) {
108 | baud ??= this.baud;
109 | await this.port.open({ baudRate: baud });
110 | }
111 |
112 | async closeDevice() {
113 | if (this.#readLoop) {
114 | this.#active = false;
115 | this.#reader.cancel();
116 | await this.#readLoop;
117 | } else {
118 | return Promise.resolve();
119 | }
120 | }
121 |
122 | usbReceive(src) {
123 | const mxTagSize = 17;
124 |
125 | let dst = this.dst;
126 | let dstIndex = this.dstIndex;
127 | let srcIndex = 0, machine;
128 |
129 | while (srcIndex < src.length) {
130 | if (dstIndex === dst.length) { // grow buffer
131 | dst = new Uint8Array(dst.length + 32768);
132 | dst.set(this.dst);
133 | this.dst = dst;
134 | }
135 | dst[dstIndex++] = src[srcIndex++];
136 |
137 | if (this.binary) {
138 | if (dstIndex < 2)
139 | this.binaryLength = dst[0] << 8;
140 | else if (2 === dstIndex)
141 | this.binaryLength |= dst[1];
142 | if ((2 + this.binaryLength) === dstIndex) {
143 | this.onReceive(dst.slice(2, 2 + this.binaryLength).buffer);
144 |
145 | dstIndex = 0;
146 | this.binary = false;
147 | delete this.binaryLength;
148 | }
149 | }
150 | else if ((dstIndex >= 2) && (dst[dstIndex - 2] == 13) && (dst[dstIndex - 1] == 10)) {
151 | if ((dstIndex >= mxTagSize) && (machine = XsbugSerial.matchProcessingInstruction(dst.subarray(dstIndex - mxTagSize, dstIndex)))) {
152 | if (machine.flag)
153 | this.currentMachine = machine.value;
154 | else
155 | this.currentMachine = undefined;
156 | this.binary = machine.binary;
157 | }
158 | else if ((dstIndex >= 10) && (dst[dstIndex - 10] == '<'.charCodeAt()) &&
159 | (dst[dstIndex - 9] == '/'.charCodeAt()) && (dst[dstIndex - 8] == 'x'.charCodeAt()) &&
160 | (dst[dstIndex - 7] == 's'.charCodeAt()) && (dst[dstIndex - 6] == 'b'.charCodeAt()) &&
161 | (dst[dstIndex - 5] == 'u'.charCodeAt()) && (dst[dstIndex - 4] == 'g'.charCodeAt()) &&
162 | (dst[dstIndex - 3] == '>'.charCodeAt())) {
163 | const message = new TextDecoder().decode(dst.subarray(0, dstIndex));
164 | // console.log(message);
165 | this.onReceive(message);
166 | }
167 | else {
168 | dst[dstIndex - 2] = 0;
169 | //@@ if (offset > 2) fprintf(stderr, "%s\n", self->buffer);
170 | }
171 | dstIndex = 0;
172 |
173 | }
174 | }
175 |
176 | this.dstIndex = dstIndex;
177 | }
178 |
179 | onReceive(data) {
180 | if ("string" === typeof data) {
181 | const msg = new XsbugMessageEx((new DOMParser).parseFromString(data, "application/xml"));
182 | if (msg.break)
183 | this.onBreak(msg);
184 | else if (msg.login)
185 | this.onLogin(msg);
186 | else if (msg.instruments)
187 | this.onInstrumentationConfigure(msg);
188 | else if (msg.local)
189 | this.onLocal(msg);
190 | else if (msg.log)
191 | this.onLog(msg);
192 | else if (msg.samples)
193 | this.onInstrumentationSamples(msg);
194 | else if (msg.bubble)
195 | this.onBubble(msg);
196 | else
197 | debugger; // unhandled
198 | }
199 | else {
200 | const view = new DataView(data);
201 | switch (view.getUint8(0)) {
202 | case 5:
203 | const id = view.getUint16(1), code = view.getInt16(3);
204 | const index = this.pending.findIndex(pending => id === pending.id)
205 | if (index >= 0) {
206 | const pending = this.pending[index];
207 | this.pending.splice(index, 1);
208 | (pending.callback)(code, data.slice(5));
209 | }
210 | break;
211 | default:
212 | debugger;
213 | break;
214 | }
215 | }
216 | }
217 |
218 |
219 | static matchProcessingInstruction(dst) {
220 | let flag, binary = false, value = 0;
221 | if (dst[0] != '<'.charCodeAt())
222 | return;
223 | if (dst[1] != '?'.charCodeAt())
224 | return;
225 | if (dst[2] != 'x'.charCodeAt())
226 | return;
227 | if (dst[3] != 's'.charCodeAt())
228 | return;
229 | let c = dst[4];
230 | if (c == '.'.charCodeAt())
231 | flag = true;
232 | else if (c == '-'.charCodeAt())
233 | flag = false;
234 | else if (c == '#'.charCodeAt()) {
235 | flag = true;
236 | binary = true;
237 | }
238 | else
239 | return;
240 | for (let i = 0; i < 8; i++) {
241 | c = dst[5 + i]
242 | if (('0'.charCodeAt() <= c) && (c <= '9'.charCodeAt()))
243 | value = (value * 16) + (c - '0'.charCodeAt());
244 | else if (('a'.charCodeAt() <= c) && (c <= 'f'.charCodeAt()))
245 | value = (value * 16) + (10 + c - 'a'.charCodeAt());
246 | else if (('A'.charCodeAt() <= c) && (c <= 'F'.charCodeAt()))
247 | value = (value * 16) + (10 + c - 'A'.charCodeAt());
248 | else
249 | return;
250 | }
251 | if (dst[13] != '?'.charCodeAt())
252 | return;
253 | if (dst[14] != '>'.charCodeAt())
254 | return;
255 | return { value: value.toString(16).padStart(8, "0"), flag, binary };
256 | }
257 |
258 | async send(data) {
259 | if ("string" == typeof data) {
260 | const preamble = XsbugConnection.crlf + `` + XsbugConnection.crlf;
261 | data = new TextEncoder().encode(preamble + data);
262 | // tracePacket("<", data);
263 | await this.#write(data);
264 | }
265 | else {
266 | let preamble = XsbugConnection.crlf + ``;
267 | preamble = new TextEncoder().encode(preamble);
268 | let payload = new Uint8Array(data);
269 | let buffer = new Uint8Array(preamble.length + 2 + payload.length);
270 | buffer.set(preamble, 0);
271 | buffer[preamble.length] = (payload.length >> 8) & 0xff;
272 | buffer[preamble.length + 1] = payload.length & 0xff;
273 | buffer.set(payload, preamble.length + 2);
274 |
275 | // tracePacket("< ", buffer);
276 | await this.#write(buffer);
277 | }
278 | }
279 |
280 | async #write(data) {
281 |
282 | let port = this.port;
283 | let writer;
284 | let pass;
285 |
286 | if (!port)
287 | return Promise.reject(Error("Port is not defined!"));
288 |
289 | while (!pass) {
290 | if (!port.writable.locked) {
291 | try {
292 | console.log("#write: @get")
293 | writer = port.writable.getWriter();
294 | console.log("#write: locked")
295 | pass = true;
296 | } catch (err) {
297 | // TypeError: Failed to execute 'getWriter' on 'WritableStream': Cannot create writer when WritableStream is locked
298 | if (err instanceof TypeError == false) {
299 | // sth else failed.
300 | throw(err);
301 | }
302 | }
303 | }
304 |
305 | if (!pass)
306 | await this.timeout(100);
307 |
308 | }
309 |
310 | writer.write(data);
311 | writer.releaseLock();
312 | console.log("#write: released")
313 |
314 | }
315 |
316 | // async write(data) {
317 |
318 | // console.log("@#write")
319 |
320 | // let port = this.port;
321 |
322 | // if (port) {
323 |
324 | // let writer;
325 |
326 | // let x = new Promise((resolve, reject) => {
327 |
328 | // // let writer;
329 | // let pass;
330 |
331 | // while (!pass) {
332 | // if (!port.writable.locked) {
333 | // try {
334 | // console.log("#write: @get")
335 |
336 | // // despite .locked == false, getWriter could fail!
337 | // writer = port.writable.getWriter();
338 | // pass = true;
339 | // } catch (err) {
340 | // // TypeError: Failed to execute 'getWriter' on 'WritableStream': Cannot create writer when WritableStream is locked
341 | // if (err instanceof TypeError == false) {
342 | // // sth else failed.
343 | // reject(err);
344 | // }
345 | // }
346 | // } else {
347 | // await this.timeout(100);
348 | // console.log("#write: locked")
349 | // }
350 | // }
351 | // console.log("resolving");
352 | // resolve();
353 | // }).then(() => {
354 | // console.log("#write: writing")
355 |
356 | // // data shall be an Uint8Array
357 | // return writer.write(data);
358 |
359 | // }).then(() => {
360 |
361 | // // Allow the serial port to be closed later.
362 | // writer.releaseLock();
363 | // console.log("#write: released")
364 | // return;
365 | // })
366 |
367 | // // return x;
368 |
369 | // } else {
370 | // // should we better resolve here?
371 | // return Promise.reject(Error("Port is not defined!"));
372 | // }
373 | // }
374 |
375 | async timeout(ms) {
376 | return new Promise((resolve) =>
377 | setTimeout(resolve, ms)
378 | );
379 | }
380 |
381 | doSetTime(time, timezoneoffset, dstoffset) {
382 | const payload = new DataView(new ArrayBuffer(12));
383 | payload.setUint32(0, time, false); // big endian
384 |
385 | if (!timezoneoffset) {
386 | timezoneoffset = new Date('November 1, 2020 00:00:00').getTimezoneOffset();
387 | timezoneoffset *= -60; // seconds
388 | console.log(timezoneoffset);
389 | }
390 |
391 | // this needs to be verified at DST times...
392 | if (!dstoffset) {
393 | let todayoffset = new Date().getTimezoneOffset();
394 | todayoffset *= -60;
395 | dstoffset = timezoneoffset - todayoffset;
396 | console.log(dstoffset);
397 | }
398 |
399 | payload.setUint32(4, timezoneoffset, false);
400 | payload.setUint32(8, dstoffset, false);
401 |
402 | this.sendBinaryCommand(9, payload);
403 | }
404 |
405 | }
406 |
--------------------------------------------------------------------------------
/resources/mcu_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ralphwetzel/node-red-mcu-plugin/05b86bf13f7eef638d3e1ece7a9320709d28219c/resources/mcu_example.png
--------------------------------------------------------------------------------
/resources/mcu_panel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ralphwetzel/node-red-mcu-plugin/05b86bf13f7eef638d3e1ece7a9320709d28219c/resources/mcu_panel.png
--------------------------------------------------------------------------------
/templates/main.js.eta:
--------------------------------------------------------------------------------
1 | import "nodered"; // import for global side effects
2 | import Modules from "modules";
3 | import config from "mc/config";
4 | import Timer from "timer";
5 | <% it.imports?.forEach(function(imp){ %>
6 | import "<%= imp %>";
7 | <% }) %>
8 |
9 | if (!Modules.has("flows")) {
10 |
11 | if (config.noderedmcu?.editor) {
12 | trace.left('{"state": "mod_waiting"}', "NR_EDITOR");
13 | } else {
14 | trace("No flows installed.\n");
15 | }
16 |
17 | } else {
18 |
19 | Timer.set(function() { // run on an empty stack
20 |
21 | const flows = Modules.importNow("flows");
22 | RED.build(flows);
23 |
24 | if (globalThis.REDTheme) {
25 |
26 | // This guard isn't really necessary
27 | // as the runtime disables all unsupported nodes!
28 | if (!Modules.has("ui_nodes") || !Modules.has("ui_templates")) {
29 | trace("flow neeeds UI nodes; not build into host \n");
30 | } else {
31 |
32 | const buildModel = Modules.importNow("ui_nodes");
33 | const templates = Modules.importNow("ui_templates");
34 | const REDApplication = templates.REDApplication;
35 | if (REDApplication) {
36 | try {
37 | const model = buildModel();
38 | new REDApplication(model, { commandListLength: <%= it.cll %>, displayListLength: <%= it.dll %>, touchCount: <%= it.tc %>, pixels: <%= it.pixels %> });
39 | }
40 | catch {}
41 | }
42 |
43 | }
44 | }
45 | });
46 | }
47 |
48 |
--------------------------------------------------------------------------------
/templates/main_mod_host_ui_js.eta:
--------------------------------------------------------------------------------
1 | import "nodered"; // import for global side effects
2 | import Modules from "modules";
3 | import config from "mc/config";
4 |
5 | if (!Modules.has("flows")) {
6 |
7 | if (config.noderedmcu?.editor) {
8 | trace.left('{"state": "mod_waiting"}', "NR_EDITOR");
9 | } else {
10 | trace("No flows installed.\n");
11 | }
12 |
13 | } else {
14 |
15 | const flows = Modules.importNow("flows");
16 | RED.build(flows);
17 |
18 | if (globalThis.REDTheme) {
19 |
20 | // This guard isn't really necessary
21 | // as the runtime disables all unsupported nodes!
22 | if (!Modules.has("ui_nodes") || !Modules.has("ui_templates")) {
23 | trace("flow neeeds UI nodes; not build into host \n");
24 | } else {
25 |
26 | const buildModel = Modules.importNow("ui_nodes");
27 | const templates = Modules.importNow("ui_templates");
28 | const REDApplication = templates.REDApplication;
29 | if (REDApplication) {
30 | try {
31 | const model = buildModel();
32 | new REDApplication(model, { commandListLength: <%= it.cll %>, displayListLength: <%= it.dll %>, touchCount: <%= it.tc %>, pixels: <%= it.pixels %> });
33 | }
34 | catch {}
35 | }
36 |
37 | }
38 | }
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/test/junction_resolver_test.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "45f8a43013ccf006",
4 | "type": "tab",
5 | "label": "Junction Test",
6 | "disabled": false,
7 | "info": "",
8 | "env": [],
9 | "_mcu": {
10 | "mcu": true
11 | }
12 | },
13 | {
14 | "id": "44806d18878baea6",
15 | "type": "junction",
16 | "z": "45f8a43013ccf006",
17 | "x": 640,
18 | "y": 280,
19 | "wires": [
20 | [
21 | "e5b569fddd3383bf"
22 | ]
23 | ]
24 | },
25 | {
26 | "id": "36a73cf7c69cc380",
27 | "type": "junction",
28 | "z": "45f8a43013ccf006",
29 | "x": 640,
30 | "y": 440,
31 | "wires": [
32 | [
33 | "4288baa6ec0a85cc"
34 | ]
35 | ]
36 | },
37 | {
38 | "id": "4288baa6ec0a85cc",
39 | "type": "junction",
40 | "z": "45f8a43013ccf006",
41 | "x": 580,
42 | "y": 360,
43 | "wires": [
44 | [
45 | "e5b569fddd3383bf"
46 | ]
47 | ]
48 | },
49 | {
50 | "id": "e5b569fddd3383bf",
51 | "type": "junction",
52 | "z": "45f8a43013ccf006",
53 | "x": 720,
54 | "y": 360,
55 | "wires": [
56 | [
57 | "d68f7841c4887aee",
58 | "49c9264aa5ecda08",
59 | "36a73cf7c69cc380"
60 | ]
61 | ]
62 | },
63 | {
64 | "id": "2998720adfc32082",
65 | "type": "junction",
66 | "z": "45f8a43013ccf006",
67 | "x": 720,
68 | "y": 200,
69 | "wires": [
70 | [
71 | "d68f7841c4887aee",
72 | "8fe0697525551151"
73 | ]
74 | ]
75 | },
76 | {
77 | "id": "8fe0697525551151",
78 | "type": "debug",
79 | "z": "45f8a43013ccf006",
80 | "name": "3",
81 | "active": true,
82 | "tosidebar": true,
83 | "console": false,
84 | "tostatus": false,
85 | "complete": "true",
86 | "targetType": "full",
87 | "statusVal": "",
88 | "statusType": "auto",
89 | "_mcu": {
90 | "mcu": true
91 | },
92 | "x": 850,
93 | "y": 200,
94 | "wires": []
95 | },
96 | {
97 | "id": "d68f7841c4887aee",
98 | "type": "debug",
99 | "z": "45f8a43013ccf006",
100 | "name": "4",
101 | "active": true,
102 | "tosidebar": true,
103 | "console": false,
104 | "tostatus": false,
105 | "complete": "true",
106 | "targetType": "full",
107 | "statusVal": "",
108 | "statusType": "auto",
109 | "_mcu": {
110 | "mcu": true
111 | },
112 | "x": 850,
113 | "y": 280,
114 | "wires": []
115 | },
116 | {
117 | "id": "49c9264aa5ecda08",
118 | "type": "debug",
119 | "z": "45f8a43013ccf006",
120 | "name": "5",
121 | "active": true,
122 | "tosidebar": true,
123 | "console": false,
124 | "tostatus": false,
125 | "complete": "true",
126 | "targetType": "full",
127 | "statusVal": "",
128 | "statusType": "auto",
129 | "_mcu": {
130 | "mcu": true
131 | },
132 | "x": 850,
133 | "y": 360,
134 | "wires": []
135 | },
136 | {
137 | "id": "3d480bbb10567fc0",
138 | "type": "inject",
139 | "z": "45f8a43013ccf006",
140 | "name": "",
141 | "props": [
142 | {
143 | "p": "payload"
144 | },
145 | {
146 | "p": "topic",
147 | "vt": "str"
148 | }
149 | ],
150 | "repeat": "",
151 | "crontab": "",
152 | "once": false,
153 | "onceDelay": 0.1,
154 | "topic": "",
155 | "payload": "1",
156 | "payloadType": "str",
157 | "_mcu": {
158 | "mcu": true
159 | },
160 | "x": 470,
161 | "y": 200,
162 | "wires": [
163 | [
164 | "44806d18878baea6",
165 | "2998720adfc32082"
166 | ]
167 | ]
168 | },
169 | {
170 | "id": "d76b363eae7f9d79",
171 | "type": "inject",
172 | "z": "45f8a43013ccf006",
173 | "name": "",
174 | "props": [
175 | {
176 | "p": "payload"
177 | },
178 | {
179 | "p": "topic",
180 | "vt": "str"
181 | }
182 | ],
183 | "repeat": "",
184 | "crontab": "",
185 | "once": false,
186 | "onceDelay": 0.1,
187 | "topic": "",
188 | "payload": "2",
189 | "payloadType": "str",
190 | "_mcu": {
191 | "mcu": true
192 | },
193 | "x": 470,
194 | "y": 360,
195 | "wires": [
196 | [
197 | "4288baa6ec0a85cc",
198 | "44806d18878baea6"
199 | ]
200 | ]
201 | }
202 | ]
--------------------------------------------------------------------------------