@ralphwetzel/node-red-mcu-plugin
Version:
Plugin to integrate Node-RED MCU Edition into the Node-RED Editor
1,472 lines (1,215 loc) • 87.8 kB
JavaScript
/*
node-red-mcu-plugin by @ralphwetzel
https://github.com/ralphwetzel/node-red-mcu-plugin
License: MIT
*/
const clone = require("clone");
// const { exec } = require('node:child_process'); // <== Node16
const { exec, execFile, execSync } = require('child_process'); // Node14
const fs = require('fs-extra');
const os = require("os");
const path = require("path");
const {SerialPort} = require("serialport");
const Eta = require("eta");
const app_name = "node-red-mcu-plugin";
const mcuProxy = require("./lib/proxy.js");
const mcuNodeLibrary = require("./lib/library.js");
const mcuManifest = require("./lib/manifest.js");
const mcuMessageRelay = require("./lib/relay.js")
// ***** AbortController
// node@14: Established w/ 14.17; polyfill to be sure
// node@16+: Fully integrated
if (!globalThis.AbortController) {
const { AbortController } = require("node-abort-controller");
globalThis.AbortController = AbortController;
}
// https://github.com/stefanpenner/resolve-package-path
const resolve_package_path = require('resolve-package-path')
let flows2build = [];
let proxy;
let proxy_port_mcu = 5004;
let proxy_port_xsbug = 5002;
let proxy_port_xsbug_log = 50002;
let error_header = "*** Error while loading node-red-mcu-plugin:"
const mcu_plugin_config = {
// "simulators": {},
"cache_file": "",
"cache_data": [],
"platforms": [],
"ports": []
}
const library = new mcuNodeLibrary.library();
global.registerMCUModeType = function(standard_type, mcumode_type) {
library.register_mcumode_type(standard_type, mcumode_type)
}
let runtime_nodes;
// ****
// Patch support function: Calculate the path to a to-be-required file
function get_require_path(req_path) {
let rm = require.main.path;
try {
let stat = fs.lstatSync(rm);
if (!stat.isDirectory()) {
console.log(error_header);
console.log("require.main.path is not a directory.");
return;
}
} catch (err) {
console.log(error_header);
if (error_header.code == 'ENOENT') {
console.log("require.main.path not found.");
} else {
console.log("Error while handling require.main.path.")
}
return null;
}
// split path into segments ... the safe way
rm = path.normalize(rm);
let rms = []
let rmp;
do {
rmp = path.parse(rm);
if (rmp.base.length > 0) {
rms.unshift(rmp.base);
rm = rmp.dir;
}
} while (rmp.base.length > 0)
let rmsl = rms.length;
/* This are some known patterns:
require.main.path:
dev: [...]/node-red/packages/node_modules/node-red
install: [...]/lib/node_modules/node-red
pi: /lib/node_modules/node-red/
docker: /usr/src/node-red/node_modules/node-red
target:
dev: [...]/node-red/packages/node_modules/@node-red
install: [...]/lib/node_modules/node-red/node_modules/@node-red
pi: /lib/node_modules/node-red/node_modules/@node-red
docker: /usr/src/node-red/node_modules/@node-red
*/
if (rms.includes("packages")) {
if (rms[rmsl-3]=="packages" && rms[rmsl-2]=="node_modules" && rms[rmsl-1]=="node-red") {
rms.splice(-2);
}
} else if (rms[0]=="usr" && rms[1]=="src" && rms[2]=="node-red" && rmsl==5) {
rms.splice(-2);
}
// compose things again...
req_path = req_path.split("/");
let p = path.join(rmp.root, ...rms, ...req_path);
if (!fs.existsSync(p)) {
console.log(error_header)
console.log("Failed to calculate correct patch path.");
console.log("Please raise an issue @ our GitHub repository, stating the following information:");
console.log("> require.main.path:", require.main.path);
console.log("> utils.js:", p);
return null;
}
return p;
}
// End: "Patch support ..."
// *****
// *****
// Make available the Node-RED typeRegistry
const typeRegistryPath = get_require_path("node_modules/@node-red/registry");
if (!typeRegistryPath) return;
const typeRegistry = require(typeRegistryPath);
// *****
// Apply patch to get access to additional node related information
// This has to happen immediately when this file is required, before any Node-RED logic kicks in...
const registryUtilPath = get_require_path("node_modules/@node-red/registry/lib/util.js")
if (!registryUtilPath) return;
const registryUtil = require(registryUtilPath)
const orig_createNodeApi = registryUtil.createNodeApi;
function patched_createNodeApi(node) {
if (node.file.indexOf("mcu_plugin.js") >= 0) {
} else {
if (node.types) {
library.register_node(node);
}
}
return orig_createNodeApi(node);
}
registryUtil.createNodeApi = patched_createNodeApi
// *** THIS DOESNT WORK!!
// We use this patch to get our hand on the full runtime.nodes API
let orig_copyObjectProperties = registryUtil.copyObjectProperties;
// console.log(orig_copyObjectProperties);
function patched_copyObjectProperties(src,dst,copyList,blockList) {
if (!runtime_nodes && copyList.indexOf("createNode") >=0 && copyList.indexOf("getNode") >=0) {
runtime_nodes = src;
// console.log(runtime_nodes);
}
return orig_copyObjectProperties(src,dst,copyList,blockList);
}
// registryUtil.copyObjectProperties = patched_copyObjectProperties;
//
// *****
// *****
// *
// * Currently used EXPERIMENTAL Flags:
// *
// * 1: Mod Build Support
// * 2: -- (next free)
// * 4: ...
//
// * => This is tested as bit field!
// *****
let MCU_EXPERIMENTAL = process.env['MCU_EXPERIMENTAL'];
let __VERSIONS__ = {};
module.exports = function(RED) {
// *****
// Say hello ...
try {
let mcu_dir = path.resolve(__dirname, "./node-red-mcu");
let git_describe = "git describe --abbrev=7 --always --long";
let mcu_version = execSync(git_describe, {"cwd": mcu_dir, input: "describe --abbrev=7 --always --long", encoding: "utf-8"});
if (typeof mcu_version == "string" && mcu_version.length > 0) {
__VERSIONS__['runtime'] = mcu_version.trim();
RED.log.info(`Node-RED MCU Edition Runtime Version: #${__VERSIONS__.runtime}`);
}
let my_package_json = require("./package.json");
__VERSIONS__['plugin'] = my_package_json.version;
RED.log.info(`Node-RED MCU Edition Plugin Version: v${__VERSIONS__.plugin}`);
} catch {}
// End: Say hello...
// *****
// *****
// env variable settings: Ensure ...
// ... that $MODDABLE is defined.
// path.normalize ensures correct slash type (see issue #11)
const MODDABLE = process.env.MODDABLE ? path.normalize(process.env.MODDABLE) : undefined;
if (!MODDABLE) {
RED.log.error("*** node-red-mcu-plugin -> Error:");
RED.log.error("* Environment variable $MODDABLE is not defined.");
RED.log.error("* Please install the Moddable SDK according to its Getting Started Guide:");
RED.log.error("* https://github.com/Moddable-OpenSource/moddable/blob/public/documentation/Moddable%20SDK%20-%20Getting%20Started.md");
RED.log.error('* In addition please be aware that, when running Node-RED as a service (e.g. on Linux),');
RED.log.error('* "it will not have access to environment variables that are defined only in the calling process."');
RED.log.error('* Please refer to https://nodered.org/docs/user-guide/environment-variables#running-as-a-service for further support.');
RED.log.error("*** node-red-mcu-plugin -> Runtime setup canceled.");
return;
}
// ... that $MODDABLE declares a valid path.
if (!fs.existsSync(MODDABLE)) {
RED.log.error("*** node-red-mcu-plugin -> Error!");
RED.log.error("* Environment variable $MODDABLE is stating a non-existing path:");
RED.log.error(`* process.env.MODDABLE = "${MODDABLE}"`);
RED.log.error("*** node-red-mcu-plugin -> Runtime setup canceled.");
return;
}
// ... that the Moddable tools directory is included in $PATH.
{
const platform_modifier = {
darwin: "mac",
linux: "lin",
win32: "win"
}
let pm = platform_modifier[process.platform];
if (!pm) {
RED.log.error("*** node-red-mcu-plugin -> Error!");
RED.log.error("* Running on a platform not supported:");
RED.log.error(`* process.platform = "${process.platform}"`);
RED.log.error("*** node-red-mcu-plugin -> Runtime setup canceled.");
return;
}
let moddable_tools_path = path.join(MODDABLE, "build", "bin", pm, "release");
if (process.env.PATH.indexOf(moddable_tools_path) < 0) {
process.env.PATH += (process.platform === "win32" ? ";" : ":") + moddable_tools_path;
}
}
// Check version of MODDABLE tools on Windows
if (os.platform() === "win32") {
let testcmd = [
`CALL "${process.env["ProgramFiles"]}\\Microsoft Visual Studio\\2022\\Community\\VC\\Auxiliary\\Build\\vcvars64.bat" > nul`,
`cd /D ${MODDABLE}\\build\\bin\\win\\debug`,
'dumpbin /headers xsbug.exe | findstr "machine"'
].join(" && ");
try {
// This doesn't test for i64 (!!)
let test = execSync(testcmd, {"encoding": "utf-8"});
__VERSIONS__['x_win'] = (test.indexOf("(x64)") > 0) ? "64" : "32"
} catch {}
}
// Try to get the version number of the MODDABLE SDK
try {
let git_describe = "git describe --abbrev=7 --always --long";
let moddable_version = execSync(git_describe, {"cwd": MODDABLE, input: "describe --abbrev=7 --always --long", encoding: "utf-8"});
if (typeof moddable_version == "string" && moddable_version.length > 0) {
__VERSIONS__['moddable'] = moddable_version.trim();
if (__VERSIONS__.x_win) {
RED.log.info(`Moddable SDK Version: v${__VERSIONS__.moddable} (${"32" === __VERSIONS__.x_win ? "x86" : "x64"})`);
} else {
RED.log.info(`Moddable SDK Version: v${__VERSIONS__.moddable}`);
}
}
} catch {}
// get the commit hash of the main.js
try {
MAINJS = "99917a1";
let git_log = "git log -n 1 --pretty=format:%h -- main.js";
let mainjs_version = execSync(git_log, {"cwd": path.join(__dirname, "node-red-mcu"), encoding: "utf-8"});
if (mainjs_version !== MAINJS) {
RED.log.info(`*** ${app_name}: main.js version update indicated: #${MAINJS} -> #${mainjs_version}`);
}
} catch(err) {}
// End: "env variable settings ..."
// *****
// *****
// Hook node definitions
function mcu_inject(config) {
RED.nodes.createNode(this, config);
let node = this;
node.on('input', function(msg, send, done) {
if (proxy) {
proxy.send2mcu("inject", this.z, this.id);
}
return;
});
// This affects the runtime representation of the node!
if (!this._mcu) {
this._mcu = {};
}
this._mcu.reset_status_on_abort = true;
}
RED.nodes.registerType("_mcu:inject", mcu_inject);
registerMCUModeType("inject", "_mcu:inject")
function mcu_debug(config) {
let dn;
// Create a standard DebugNode
let debugNodeConstructor = typeRegistry.get("debug");
if (!debugNodeConstructor)
return;
dn = new debugNodeConstructor(config);
// patch the "active" property for getter & setter !
if ("active" in dn) {
dn._active = dn.active;
delete dn.active;
Object.defineProperty(dn, "active", {
get() {
return this._active;
},
set(status) {
this._active = status ? true : false;
if (this.__getProxy) {
let p = this.__getProxy();
if (p) {
p.send2mcu("debug", this.z, this.id, this._active);
}
}
}
})
}
// This affects the runtime representation of the node!
if (!dn._mcu) {
dn._mcu = {};
}
dn._mcu.reset_status_on_abort = true;
return dn;
}
RED.nodes.registerType("_mcu:debug", mcu_debug);
registerMCUModeType("debug", "_mcu:debug")
// We use this node if no replacement is defined.
// This gives us access to the basic functionality of a node, like emitting warnings & errors.
function mcu_void(config) {
// Let's give back this voided node it's original type!
if ("void" in config) {
config.type = config.void;
}
RED.nodes.createNode(this, config);
// This affects the runtime representation of the node!
if (!this._mcu) {
this._mcu = {};
}
this._mcu.reset_status_on_abort = true;
}
RED.nodes.registerType("_mcu:void", mcu_void);
// End "Hook ..."
// *****
// *****
// Calculate path to flowUtil (lib/flows/util.js) & require it
let flowUtilPath = get_require_path("node_modules/@node-red/runtime/lib/flows/util.js")
if (!flowUtilPath) return;
let flowUtil = require(flowUtilPath)
// End "Calculate ..."
// *****
// ****
// Instance to forward messages from the MCU into runtime
let mcuRelay = new mcuMessageRelay.relay(RED);
// *****
// Apply a patch to hook into the node creation process of the runtime.
function getProxy() {
if (proxy) return proxy;
}
let orig_createNode = flowUtil.createNode;
async function patched_createNode(flow,config) {
let orig_type = config.type;
let give_proxy = false;
if (config._mcu?.mcu === true) {
if (config.type) {
let t = library.get_mcumode_type(config.type)
if (t) {
// replacing original node w/ _mcu:... node
config.type = t;
give_proxy = true;
} else {
// if no replacement node defined: Save the original type in config.void...
config.void = config.type;
// ... and create the void replacement node!
config.type = "_mcu:void";
}
}
}
let node = await orig_createNode(flow, config);
// give mcu replacement nodes access to the proxy
if (give_proxy) {
node.__getProxy = getProxy;
}
return node;
}
// Only for debugging
let orig_diffConfigs = flowUtil.diffConfigs;
function patched_diffConfigs(oldConfig, newConfig) {
let res = orig_diffConfigs(oldConfig, newConfig);
// console.log("diffConfigs", res);
return res;
}
let orig_diffNodes = flowUtil.diffNodes;
function patched_diffNodes(oldNode,newNode) {
let res = orig_diffNodes(oldNode,newNode);
// console.log("diffNodes", res);
return res;
}
flowUtil.createNode = patched_createNode;
flowUtil.diffNodes = patched_diffNodes;
flowUtil.diffConfigs = patched_diffConfigs;
// End "Apply..."
// *****
// ***** RDW221201: obsolete
// function patch_xs_file(pre, post) {
// let moddable = process.env.MODDABLE
// if (moddable) {
// let os_file = {
// "darwin": "mac_xs.c"
// }
// let xs_file_path = path.join(moddable, 'xs', 'platforms', os_file[process.platform]);
// let xs_file = fs.readFileSync(xs_file_path).toString();
// let check_pre = "address.sin_port = htons(" + pre + ");";
// let check_post = "address.sin_port = htons(" + post + ");";
// if (xs_file.indexOf(check_pre) > 0) {
// xs_file = xs_file.replace(check_pre, check_post);
// }
// if (xs_file.indexOf(check_post) < 0) {
// throw "Failed to patch platform specific debug connection.";
// } else {
// console.log("Patch success confirmed @ " + post + ".");
// fs.writeFileSync(xs_file_path, xs_file);
// }
// } else {
// throw "Cannot proceed. $MODDABLE is not defined.";
// }
// return;
// }
// *****
// *****
// File to persist plugin data
function get_cache() {
let cache_file = path.join(RED.settings.userDir, "mcu-plugin-cache", "cache.json");
fs.ensureFileSync(cache_file);
let cache_data;
try {
cache_data = fs.readFileSync(cache_file, 'utf8');
} catch (err) {
RED.log.error(`${app_name}: Failed to open cache file @ ${cache_file}.`);
} finally {
cache_data = (cache_data.length > 0) ? cache_data : "[]"
}
try {
cache_data = JSON.parse(cache_data) || {};
} catch (err) {
RED.log.warn(`${app_name}: Cache file corrupted @ ${cache_file}.`);
}
mcu_plugin_config.cache_file = cache_file;
mcu_plugin_config.cache_data = cache_data;
}
function persist_cache(data) {
if (!data) {
data = mcu_plugin_config.cache_data;
} else {
mcu_plugin_config.cache_data = data;
}
let cache_data = JSON.stringify(data);
fs.writeFile(mcu_plugin_config.cache_file, cache_data, err => {
if (err) {
RED.log.warn(`${app_name}: Failed to persist config to cache @ ${mcu_plugin_config.cache_file}.`);
}
})
}
get_cache();
// End: "File ..."
// *****
// *****
// Collect some info regarding the MODDABLE toolkit
// https://stackoverflow.com/questions/18112204/get-all-directories-within-directory-nodejs
function getDirectories(parent_dir) {
return fs.readdirSync(parent_dir).filter(function (file) {
return fs.statSync(path.join(parent_dir,file)).isDirectory();
});
}
{
// Those are the available platforms we are aware of:
let platform_identifiers = [
'esp/8285',
'esp/adafruit_oled',
'esp/adafruit_st7735',
'esp/buydisplay_ctp',
'esp/crystalfontz_monochrome_epaper',
'esp/esp8266_st7789',
'esp/generic_square_huzzah',
'esp/moddable_display_1',
'esp/moddable_display_3',
'esp/moddable_one',
'esp/moddable_three',
'esp/moddable_zero',
'esp/nodemcu',
'esp/sharp_memory',
'esp/sharp_memory_square',
'esp/sparkfun_teensyview',
'esp/switch_science_reflective_lcd',
'esp32/c3_32s_kit',
'esp32/c3_32s_kit_2m',
'esp32/c3_devkit_rust',
'esp32/esp32_st7789',
'esp32/esp32_thing',
'esp32/esp32_thing_plus',
'esp32/esp32c3',
'esp32/esp32c3_cdc',
'esp32/esp32c6',
'esp32/esp32c6_cdc',
'esp32/esp32c6_gc9a01',
'esp32/esp32h2',
'esp32/esp32h2_cdc',
'esp32/esp32h2_ili9341',
'esp32/esp32s3',
'esp32/esp32s3_cdc',
'esp32/esp32s3_usb',
'esp32/feather_s3_tft',
'esp32/heltec_lora_32',
'esp32/heltec_wifi_kit_32',
'esp32/kaluga',
'esp32/lilygo_t5s',
'esp32/lilygo_t_qt',
'esp32/lilygo_t_camera_plus_s3',
'esp32/lilygo_tdisplay_s3',
'esp32/lilygo_ttgo',
'esp32/lolin_c3mini',
'esp32/lolin_c3pico',
'esp32/lolin_s2mini',
'esp32/m5atom_echo',
'esp32/m5atom_lite',
'esp32/m5atom_matrix',
'esp32/m5atom_s3',
'esp32/m5atom_s3_lite',
'esp32/m5atom_s3r',
'esp32/m5atom_s3_org',
'esp32/m5atom_u',
'esp32/m5core_ink',
'esp32/m5dial',
'esp32/m5nanoc6',
'esp32/m5paper',
'esp32/m5stack',
'esp32/m5stack_core2',
'esp32/m5stack_cores3',
'esp32/m5stack_fire',
'esp32/m5stamp_s3',
'esp32/m5stick_c',
'esp32/m5stick_cplus',
'esp32/moddable_display_2',
'esp32/moddable_display_6',
'esp32/moddable_six',
'esp32/moddable_six_cdc',
'esp32/moddable_two',
'esp32/moddable_two_io',
'esp32/moddable_two_io_v5',
'esp32/moddable_zero',
'esp32/nodemcu',
'esp32/oddwires',
'esp32/qtpyc3',
'esp32/qtpyc3_ili9341',
'esp32/qtpys2',
'esp32/qtpys2_ili9341',
'esp32/qtpys3',
'esp32/qtpys3_cdc',
'esp32/qtpys3_ili9341',
'esp32/saola_wroom',
'esp32/saola_wrover',
'esp32/wemos_oled_lolin32',
'esp32/wrover_kit',
'esp32/wt32_eth01',
'esp32/xiao_esp32c3',
'esp32/xiao_esp32c3_ili9341',
'esp32/xiao_esp32s3',
'esp32/xiao_esp32s3_ili9341',
'esp32/xiao_esp32s3_sense',
'gecko/blue',
'gecko/giant',
'gecko/mighty',
'gecko/thunderboard',
'gecko/thunderboard2',
'nrf52/dk',
'nrf52/itsybitsy',
'nrf52/makerdiary_nrf52',
'nrf52/moddable_display_4',
'nrf52/moddable_four',
'nrf52/moddable_four_io',
'nrf52/moddable_four_uart',
'nrf52/sparkfun',
'nrf52/xiao',
'nrf52/xiao_ili9341',
'pico/captouch',
'pico/ili9341',
'pico/ili9341_i2s',
'pico/itsybitsy',
'pico/lilygo_t_display',
'pico/pico_2',
'pico/pico_display',
'pico/pico_display_2',
'pico/pico_lcd_1.3',
'pico/pico_plus_2',
'pico/pico_w',
'pico/picosystem',
'pico/pro_micro',
'pico/qt_trinkey',
'pico/qtpy',
'pico/qtpy_ili9341',
'pico/sparkfun_rp2350',
'pico/tiny2040',
'pico/ws_round',
'pico/ws_round_touch',
'pico/xiao_ili9341',
'pico/xiao_rp2040',
'qca4020/cdb'
]
let platforms = [];
let platform_path = path.join(MODDABLE, "build", "devices");
let platforms_verified = platform_identifiers.slice(0); // deep copy
let p1 = getDirectories(platform_path);
let opener = true;
for (let i=0; i<p1.length; i+=1) {
let target_path = path.join(MODDABLE, "build", "devices", p1[i], "targets");
let p2 = getDirectories(target_path);
for (let ii=0; ii<p2.length; ii+=1) {
let p = p1[i]+"/"+p2[ii];
let io = platforms_verified.indexOf(p);
if (!(io < 0)) {
platforms_verified.splice(io,1);
platforms.push({value: p})
} else {
if (opener) {
RED.log.info(`*** ${app_name}:`);
RED.log.info("It looks as if a new platform option has been added.");
RED.log.info("Please raise an issue @ our GitHub repository, stating the following information:");
opener = false;
}
RED.log.info(`> New platform: ${p}`);
}
}
}
opener = true;
for (let i=0; i<platforms_verified.length; i+=1) {
if (opener) {
RED.log.info(`*** ${app_name}:`);
RED.log.info("It looks as if a platform option has been removed.");
RED.log.info("Please raise an issue @ our GitHub repository, stating the following information:");
opener = false;
}
RED.log.info(`> Verify platform: ${platforms_verified[i]}`);
platform_identifiers.splice(platform_identifiers.indexOf(platforms_verified[i]), 1);
}
// add generic build targets
mcu_plugin_config.platforms = [];
["esp", "esp32"].forEach((p) => {
mcu_plugin_config.platforms.push({value: p})
})
mcu_plugin_config.platforms.push(...platforms);
}
{
// Those are the available sims we are aware of:
let simulator_identifiers = {
'sim/m5paper': "M5Paper",
'sim/m5stack' : "M5Stack",
'sim/m5stickc': "M5Stick",
'sim/moddable_one': "Moddable One",
'sim/moddable_two': "Moddable Two",
'sim/moddable_three': "Moddable Three", // this order looks better
'sim/moddable_four': "Moddable Four",
'sim/moddable_six': "Moddable Six",
'sim/nodemcu': "Node MCU",
'sim/pico_display': "Pico Display",
'sim/pico_display_2': "Pico Display2",
'sim/pico_ws_round': "Pico Round Display / WaveShare"
};
let platforms = mcu_plugin_config.platforms;
let simulator_path = path.join(MODDABLE, "build", "simulators");
let sims_verified = Object.keys(simulator_identifiers);
p1 = getDirectories(simulator_path);
let opener = true;
for (let i=0; i<p1.length; i+=1) {
let id = "sim/"+p1[i];
if (p1[i] !== "modules") {
if (!simulator_identifiers[id]) {
if (opener) {
RED.log.info(`*** ${app_name}:`);
RED.log.info("There seems to be an additional simulator option available.");
RED.log.info("Please raise an issue @ our GitHub repository, stating the following information:");
opener = false;
}
RED.log.info(`> New simulator: ${id}`);
} else {
sims_verified.splice(sims_verified.indexOf(id), 1);
platforms.push({value: id, label: simulator_identifiers[id]})
}
}
}
opener = true;
for (let i=0; i<sims_verified.length; i+=1) {
if (opener) {
RED.log.info(`*** ${app_name}:`);
RED.log.info("It looks as if a simulator option has been removed.");
RED.log.info("Please raise an issue @ our GitHub repository, stating the following information:");
opener = false;
}
RED.log.info("> Verify simulator:", sims_verified[i]);
delete simulator_identifiers[sims_verified[i]];
}
mcu_plugin_config.platforms = platforms;
}
// End "Collect ..."
// *****
// *****
// The serial port scanner
function refresh_serial_ports(repeat) {
SerialPort.list()
.then( (p) => {
let ports = [];
for (let i=0; i<p.length; i+=1) {
// Only process true hardware devices, reporting vendorId & productId.
// This might become an issue at some point in time ... to be addressed then!
if (!p[i].vendorId && !p[i].productId) {
continue;
}
if (p[i].path && p[i].path.length > 0) {
let pth = p[i].path;
if ("darwin" === os.platform()) {
// SerialPort (usually / only) reports the "/dev/tty." devices.
// On MacOs, we yet need the "/dev/cu."s to launch successfully!
if (-1 < pth.indexOf("/dev/tty.")) {
pth = pth.replace("/dev/tty.", "/dev/cu.")
} else {
continue;
}
}
ports.push(pth);
}
}
ports.sort();
mcu_plugin_config.ports = ports;
// ToDo: Check why the message is received several (==3) times at the client side?!
RED.comms.publish("mcu/serialports", ports, false);
setTimeout(refresh_serial_ports, repeat, repeat);
})
}
refresh_serial_ports(5000);
// End: "The serial..."
// *****
// *****
// The plugin
const apiRoot = "/mcu";
const routeAuthHandler = RED.auth.needsPermission("mcu.write");
// The (single) promise when running a MCU target
let runner_promise;
// The AbortController for runner_promise
let runner_abort;
RED.events.on("flows:stopping", (...args) => {
let flows = args[0]?.config?.flows;
if (flows && Array.isArray(flows)) {
for (let i=0; i<flows.length; i++) {
let f = flows[i];
if (f?._mcu?.mcu) {
// abort the currently running runner
if (runner_promise && runner_abort) {
runner_abort.abort();
delete runner_promise;
delete runner_abort;
if (proxy) {
proxy.disconnect();
delete proxy;
}
RED.log.info("MCU: Aborting active server side debugging session.");
}
break;
}
}
}
})
function consolidate_mcu_nodes(with_ui_support) {
// Select the nodes to build flows.json
let nodes = [];
let configNodes = {};
// Very special node - providing base functionality for dashboard!
// This node is not referenced by any other node;
// thus we have to catch it when we stumble upon it!
let ui_base;
// identify the nodes flagged with _mcu & as well the config nodes
RED.nodes.eachNode(function(nn) {
// Catch ui_base
if (nn.type == "ui_base") {
ui_base = clone(nn);
return;
}
// The "official" test for a config node!
// This as well pushes "ui_group" & "ui_tab" nodes into configNodes
if (!nn.hasOwnProperty('x') && !nn.hasOwnProperty('y')) {
configNodes[nn.id] = { "node": clone(nn) };
}
if (nn._mcu?.mcu === true) {
let n = clone(nn);
// ToDo: We have to find an alternative logic for this!!
let running_node = RED.nodes.getNode(n.id);
running_node?.emit("mcu:plugin:build:prepare", n, nodes);
nodes.push(n);
}
});
/*****
* Resolve junction node connections to target nodes.
* Resolve as well (standard) Link Out/In connections.
*
* This could affect the total number of connection per output.
* This code as well gets rid of circular references ... in case someone tries to play with the engine ;)
*/
let resolver_cache = {};
// initialize the resolver cache
nodes.forEach(function (n) {
resolver_cache[n.id] = n;
})
function resolve_wire(dest, path) {
function getNode(id) {
// first check the resolver cache
let node = resolver_cache[id];
if (!node) {
// try to get running instance of this id
let n = RED.nodes.getNode(id);
if (!n) {
// That's sh** !
console.log(`Wires Resolver: Couldn't get node definition for #${id}.`)
return;
}
// create representation
node = {
"id": id,
"type": n.type
}
if (n.wires) {
node['wires'] = clone(n.wires);
}
resolver_cache[id] = node;
}
return node;
}
let node = getNode(dest);
if (!node) return;
let wires;
switch (node.type) {
case "link in":
case "junction":
// shall exactly have one output!
if (!node.wires || !Array.isArray(node.wires) || node.wires.length < 1) {
return; // doesn't hurt
}
if (node.wires.length > 1) {
console.log(`Wires Resolver: Node #${id} (${node.type}) seems to have more than one output?!`);
return;
}
wires = node.wires[0];
break;
case "link out":
if (node.mode === "link") {
wires = node.links;
if (!wires || !Array.isArray(wires) || wires.length < 1)
return;
break;
}
// link.mode == "call" => treat as normal node!
default:
return [dest];
}
// node IS (!) a junction or Link Out/In; continue resolving!
if (wires.length == 0) {
return;
}
let selfpath = path ? new Set([...path]) : new Set();
selfpath.add(dest);
let resolved = [];
// flag if we hit a circular reference from here
let path_hit = false;
for (let i = 0, l = wires.length; i < l; i++) {
let wire = wires[i];
if (selfpath.has(wire)) {
path_hit = true;
continue; // break the circle reference
}
let res = resolve_wire(wire, selfpath);
if (res) {
resolved.push(...res);
}
}
if (!path_hit)
node.wires[0] = resolved;
return resolved;
}
// resolve junction nodes & link nodes (out -> in) to wires
nodes.forEach(function (node) {
if (node.type !== "tab" && ("wires" in node)) {
let resolved_wires = [];
for (let output = 0, l = node.wires.length; output < l; output++) {
let output_wires = new Set();
for (let w = 0, lw = node.wires[output].length; w < lw; w++) {
let rw = resolve_wire(node.wires[output][w]);
if (rw) {
output_wires = new Set([...output_wires, ...rw]);
}
}
resolved_wires.push([...output_wires]);
}
node.wires = resolved_wires;
}
});
// Remove Link Out/In nodes & Junctions
nodes = nodes.filter(function(node) {
switch (node.type) {
case "link out":
return (node.mode !== "link");
case "link in":
// check if this "link in" node is target of a "link call" node!
for (let i=0, l=nodes.length; i<l; i++) {
let n = nodes[i];
if (n.type === "link call") {
if (n.links && Array.isArray(n.links) && n.links.length>0) {
if (n.links[0] === node.id){
return true;
}
}
}
}
// If not: eliminate!
return false;
case "junction":
return false;
default:
return true;
}
});
// check if config nodes are referenced
function test_for_config_node(obj) {
for (const key in obj) {
let ok = obj[key];
if (ok && typeof(ok)==="object") {
test_for_config_node(ok);
} else {
// Regex as proposed by Steve: https://github.com/ralphwetzel/node-red-mcu-plugin/commit/0cb67f85262705e2c812df6819e3ebd511189d20#commitcomment-132987108
if (key!=="id" && key!=="z" && key!=="type" && typeof(ok)==="string" && ok.match(/^[0-9a-f]{8}\.?[0-9a-f]{3,8}$/i)) {
cn = configNodes[ok];
if (cn && (cn.mcu !== true)) {
cn.mcu = true;
// recursion necessary e.g. for ui_nodes
test_for_config_node(cn);
}
}
}
}
}
for (let i=0;i<nodes.length; i++) {
test_for_config_node(nodes[i]);
}
// add config nodes to the mcu nodes
for (let key in configNodes) {
if (configNodes[key].mcu === true) {
nodes.push(configNodes[key].node);
}
}
nodes.forEach((node) => {
// check if this node manages credentials
let n = RED.nodes.getNode(node.id);
if (n) {
if (n.credentials) {
// node._mcu ??= {}; // <= node 15+
if (!node._mcu) {
node._mcu = {};
}
node._mcu["credentials"] = clone(n.credentials);
}
}
})
// Add UI support
if (with_ui_support) {
// add ui_base node to the group of nodes to be exported!
if (!ui_base) {
// There might be situations where ui_base was deleted in the editor;
// rather than throwing here, we try to create a minimal / standard replacement node
ui_base = {
id: RED.util.generateId(),
type: "ui_base",
theme: {
name: "theme-dark",
lightTheme: {
default: "#0094CE",
baseColor: "#0094CE",
baseFont: "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif",
edited: true,
reset: false
},
darkTheme: {
default: "#097479",
baseColor: "#097479",
baseFont: "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif",
edited: true,
reset: false
},
themeState: {
"base-color": {
default: "#097479",
value: "#097479",
edited: false
},
"page-titlebar-backgroundColor": {
value: "#097479",
edited: false
},
"page-backgroundColor": {
value: "#111111",
edited: false
},
"page-sidebar-backgroundColor": {
value: "#333333",
edited: false
},
"group-textColor": {
value: "#0eb8c0",
edited: false
},
"group-borderColor": {
value: "#555555",
edited: false
},
"group-backgroundColor": {
value: "#333333",
edited: false
},
"widget-textColor": {
value: "#eeeeee",
edited: false
},
"widget-backgroundColor": {
value: "#097479",
edited: false
},
"widget-borderColor": {
value: "#333333",
edited: false
},
"base-font": {
value: "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif"
}
},
angularTheme: {
primary: "indigo",
accents: "blue",
warn: "red",
background: "grey",
palette: "dark"
}
},
site: {
name: "Node-RED Dashboard",
hideToolbar: "false",
allowSwipe: "false",
lockMenu: "false",
allowTempTheme: "none",
dateFormat: "DD.MM.YYYY",
sizes: {
sx: 48,
sy: 48,
gx: 6,
gy: 6,
cx: 6,
cy: 6,
px: 0,
py: 0
}
}
}
}
nodes.push(ui_base);
}
return nodes;
}
function make_build_environment(nodes, working_directory, options) {
// Create target directory
let dest = working_directory ?? fs.mkdtempSync(path.join(os.tmpdir(), app_name));
fs.ensureDirSync(dest);
// Create and initialize the manifest builder
let mcu_nodes_root = path.resolve(__dirname, "./mcu_modules");
let manifest = new mcuManifest.builder(library, mcu_nodes_root);
manifest.initialize();
manifest.resolver_paths = [
require.main.path,
RED.settings.userDir
]
// Try to make this the first entry - before the includes!
// Add MODULES build path
const mbp = path.resolve(MODDABLE, "./modules");
manifest.add_build("MODULES", mbp);
// Add root manifest from node-red-mcu
// ToDo: node-red-mcu shall be a npm package as well - soon!
const root_manifest_path = "./node-red-mcu"
let rmp = path.resolve(__dirname, root_manifest_path);
manifest.add_build("MCUROOT", rmp);
switch (options._mode) {
case "mod":
manifest.include_manifest("$(MODDABLE)/examples/manifest_mod.json");
break;
default:
manifest.include_manifest("$(MCUROOT)/manifest_host.json");
}
// resolve core nodes directory => "@node-red/nodes"
for (let i=0; i<manifest.resolver_paths.length; i+=1) {
let pp = resolve_package_path("@node-red/nodes", manifest.resolver_paths[i]);
if (pp) {
manifest.add_build("REDNODES", path.dirname(pp));
}
}
let nodes_demanding_ui_support = 0;
// *****
// Map Node-RED node definitions to node-red-mcu core manifest.json files
// Check directories in node-red-mcu/nodes
// Latest check: 20221221/RDW
// core: Node of node-red; type -> nr_type_map
// mcu: Contrib node; module id -> mcu_module_map
// package: Has dedicated package.json; no action
// audioout => package
// batch => core
// csv => core
// delay => core
// file => core (file, file in)
// httprequest => core (http request)
// httpserver => core (hhtp in, http response)
// join => core
// lower-case => package
// openweathermap => mcu
// random => core
// rpi-ds18b20 => mcu
// rpi-gpio => mcu
// rpi-neopixels => mcu
// sensor => package
// sort => core
// split => core
// tcp => core
// template => core
// trigger => core
// udp => core (udp in, udp out)
// ui => DEDICATED HANDLING
// websocketnodes => core (websocket-client, websocket-listener, websocket in, websocket out)
function mcu_manifest(name) {
return `$(MCUROOT)/nodes/${name}/manifest.json`
}
// need to map here every type covered
const nr_type_map = {
"batch": "batch",
"csv": "csv",
"delay": "delay",
"file": "file",
"file in": "file",
"http request": "httprequest",
"http in": "httpserver",
"http response": "httpserver",
"join": "join",
"random": "random",
"sort": "sort",
"split": "split",
"tcp in": "tcp",
"tcp out": "tcp",
"template": "template",
"trigger": "trigger",
"udp in": "udp",
"udp out": "udp",
"websocket-client": "websocketnodes",
"websocket-listener": "websocketnodes",
"websocket in": "websocketnodes",
"websocket out": "websocketnodes",
}
// always map the (full) module
const mcu_module_map = {
"node-red-node-openweathermap": mcu_manifest("openweathermap"),
// https://github.com/bpmurray/node-red-contrib-ds18b20-sensor
"node-red-contrib-ds18b20-sensor": mcu_manifest("rpi-ds18b20"),
"node-red-node-pi-gpio": mcu_manifest("rpi-gpio"),
"node-red-node-pi-neopixel": mcu_manifest("rpi-neopixels"), // Att: this is pixel vs. pixel"s"
}
// UI_Nodes support
// Check if ui_nodes need to be supported
// This has to be done upfront as ui_base might already be in the nodes array ...
// ... and we need to remove it (in case it's not needed) before enumerating the manifest.json(s)
{
let nodes_demanding_ui_support = 0;
nodes.forEach((n) => {
let node = library.get_node(n.type);
if (!node) return;
let module = node.module;
if (!module) return;
if (module === "node-red-dashboard" && n.type !== "ui_base") {
if (!options.ui) {
throw Error("This flow uses UI nodes - yet UI support is diabled. Please enable UI support.")
}
nodes_demanding_ui_support += 1;
}
})
if (nodes_demanding_ui_support < 1) {
// If the operator sets "UI Support" despite it's not necessary,
// ui_base was already added.
// Thus we remove it here again if present - as not necessary!
let i = nodes.findIndex( (n) => {
return "ui_base" == n.type;
})
if (-1 < i) {
nodes.splice(i, 1);
}
}
}
// To prepare main.js
let mainjs_additional_imports = [];
let type2manifest = {};
try {
type2manifest = require(path.join(rmp, "node_types.json"));
} catch {}
// In case a node maintains credentials,
// we'll collect them here & save to credentials.json
let credentials = {}
nodes.forEach(function (n) {
// care for the credentials first
if (n._mcu?.credentials) {
credentials[n.id] = clone(n._mcu.credentials);
delete n._mcu.credentials;
}
// check _mcu for any manifest information defind
if (n._mcu?.manifest?.trim?.().length > 0) {
// Write the flow's manifest.json
fs.writeFileSync(path.join(dest, `manifest_${n.id}.json`), n._mcu.manifest.trim(), (err) => {
if (err) {
throw err;
}
});
manifest.include_manifest(`./manifest_${n.id}.json`)
}
if (n._mcu?.include && Array.isArray(n._mcu.include)) {
n._mcu.include.forEach(function(m) {
manifest.include_manifest(m);