@ralphwetzel/node-red-mcu-plugin
Version:
Plugin to integrate Node-RED MCU Edition into the Node-RED Editor
1,430 lines (1,191 loc) • 81.7 kB
HTML
<!--
/*
node-red-mcu-plugin by @ralphwetzel
https://github.com/ralphwetzel/node-red-mcu-plugin
License: MIT
*/
-->
<!-- Additional styles for red-ui-tabs-bottom -->
<style>
.red-ui-tabs.red-ui-tabs-bottom {
border-bottom: 1px solid var(--red-ui-tab-background-active);
}
.red-ui-tabs.red-ui-tabs-bottom ul {
border-top: 1px solid var(--red-ui-primary-border-color);
}
.red-ui-tabs.red-ui-tabs-bottom ul li {
margin: 0 3px 3px 3px;
top: -1px;
border-bottom: 1px solid var(--red-ui-primary-border-color);
}
.red-ui-tabs.red-ui-tabs-bottom ul li.active{
border-top: 1px solid var(--red-ui-tab-background-active);
}
.red-ui-tab-button.red-ui-tabs-bottom {
border-top: 1px solid var(--red-ui-primary-border-color);
border-bottom: 1px solid var(--red-ui-tab-background-active);
}
.mcu-build-option-label {
display: inline-block;
min-width: 140px;
}
.mcu-build-button {
height: 33px;
line-height: 20px;
font-size: 14px;
background: rgb(7, 48, 112);
color: white !important;
box-sizing: border-box;
display: inline-block;
border: 1px solid var(--red-ui-form-input-border-color);
text-align: center;
margin:0;
cursor:pointer;
}
.mcu-build-button :focus {
outline: none;
}
.mcu-build-button-button {
border-right: none;
padding-right: 12px;
padding-left: 12px;
}
.mcu-build-button-select {
line-height: 32px;
vertical-align: middle;
padding: 0px 8px;
border-left: none;
}
.mcu-build-button-select:focus {
outline: none;
}
.attention {
border-color: transparent;
outline: solid rgb(7, 48, 112) 2px;
}
.mcu-build-progress-bar {
background: rgb(7, 48, 112);
width: 0px;
height: 2px;
margin-top: 0px;
margin-left: 4px;
margin-right: 4px;
margin-bottom: 2px;
max-width: calc(100% - 8px);
}
#mcu_console::-webkit-scrollbar {
display: none;
}
</style>
<script type="text/javascript" src="resources/@ralphwetzel/node-red-mcu-plugin/lib/flashTab.js"></script>
<script type="module">
import {ESPLoader, Transport} from "./resources/@ralphwetzel/node-red-mcu-plugin/lib/espressif.js";
globalThis._mcu = {};
globalThis._mcu.ESPLoader = ESPLoader;
globalThis._mcu.Transport = Transport;
</script>
<script type="text/javascript">
let script = document.createElement('script');
script.onload = function () {
let script = document.createElement('script');
script.src = "resources/@ralphwetzel/node-red-mcu-plugin/lib/xsserial.js";
document.head.appendChild(script);
};
script.src = "resources/@ralphwetzel/node-red-mcu-plugin/lib/xsbug.js";
document.head.appendChild(script);
</script>
<script type="text/javascript">
(function() {
var sidebarContent;
var treeList;
var objects = {};
let flowData = [];
let flowList = flowData;
let flowSelector;
let panels;
let tabs;
let tabsTop;
let console_view;
let config_view;
let buildPanelHeader;
let tabsContainer;
let selectorPanel;
let selectorPanelTipStamp = 0;
let buildPanel;
let progress_bar;
function show() {
RED.sidebar.show("node-red-mcu");
}
function onFlowAdd(ws) {
objects[ws.id] = {
id: ws.id,
element: getFlowLabel(ws),
// children:[],
deferBuild: true,
icon: "red-ui-icons red-ui-icons-flow",
gutter: getGutter(ws),
checkbox: true,
selected: (ws._mcu?.mcu == true) ?? false
}
flowData.push(objects[ws.id]);
if (flowSelector) {
flowSelector.treeList('data', flowData);
}
objects[ws.id].element.toggleClass("red-ui-info-outline-item-disabled", !!ws.disabled)
objects[ws.id].treeList.container.toggleClass("red-ui-info-outline-item-disabled", !!ws.disabled)
}
function onFlowChange(n) {
var existingObject = objects[n.id];
var label = n.label || n.id;
var newlineIndex = label.indexOf("\\n");
if (newlineIndex > -1) {
label = label.substring(0,newlineIndex)+"...";
}
existingObject.element.find(".red-ui-info-outline-item-label").text(label);
existingObject.element.toggleClass("red-ui-info-outline-item-disabled", !!n.disabled)
existingObject.treeList.container.toggleClass("red-ui-info-outline-item-disabled", !!n.disabled)
}
function onFlowsReorder(order) {
flowData = [];
for (let i=0; i<order.length; i+=1) {
flowData.push(objects[order[i]]);
}
if (flowSelector) {
flowSelector.treeList('data', flowData);
}
}
function getFlowLabel(n) {
var div = $('<div>',{class:"red-ui-info-outline-item red-ui-info-outline-item-flow"});
var contentDiv = $('<div>',{class:"red-ui-search-result-description red-ui-info-outline-item-label"}).appendTo(div);
var label = (typeof n === "string")? n : n.label;
var newlineIndex = label.indexOf("\\n");
if (newlineIndex > -1) {
label = label.substring(0,newlineIndex)+"...";
}
contentDiv.text(label);
return div;
}
function getGutter(n) {
var span = $("<span>",{class:"red-ui-info-outline-gutter red-ui-treeList-gutter-float"});
return span;
}
var empties = {};
function getEmptyItem(id) {
var item = {
empty: true,
element: $('<div class="red-ui-info-outline-item red-ui-info-outline-item-empty">').text(RED._("sidebar.info.empty")),
}
empties[id] = item;
return item;
}
function getNodeLabel(n) {
var div = $('<div>',{class:"red-ui-node-list-item red-ui-info-outline-item"});
RED.utils.createNodeIcon(n, true).appendTo(div);
div.find(".red-ui-node-label").addClass("red-ui-info-outline-item-label")
addControls(n, div);
return div;
}
function onObjectRemove(n) {
var existingObject = objects[n.id];
existingObject.treeList.remove();
delete objects[n.id]
}
function resize_sidebar(top, bottom) {
let header_height = buildPanelHeader.outerHeight();
if (selectorPanel) {
top = top || $(selectorPanel).outerHeight();
}
if (buildPanel) {
bottom = bottom || $(buildPanel).outerHeight();
}
if (tabs) {
tabs.resize();
}
if (config_view) {
let tabs_height = tabs ? $(tabs).height : 0;
config_view.editableList("width", $(config_view).width());
let cvh = bottom - 88;
config_view.editableList("height", cvh < 100 ? 100 : cvh);
if (cvh < 100) {
tabsContainer.css({"top": "147px"});
} else {
tabsContainer.css({"top": "calc(100% - 36px)"});
}
}
}
function rand_id(prefix) {
return Math.random().toString(36).replace('0.', prefix || '');
}
function isEmpty(obj) {
for(var prop in obj) {
if(obj.hasOwnProperty(prop))
return false;
}
return true;
}
let build_config = [];
let build_config_default = {
platform: "sim/moddable_one",
port: "",
more: false,
buildmode: "0", // EXPERIMENTAL
buildtarget: "all",
debug: true,
debugtarget: "0",
release: false,
arguments: "{}",
creation: "{}",
ssid: "",
password: "",
ui: false,
pixel: "rgb565le",
rotation: "0",
cll: 4096, // CommandListLength
dll: 8192, // DisplayListLength
tc: 1, // TouchCount
px: 240, // Size of render buffer;
py: 32 // see: https://github.com/phoddie/node-red-mcu/discussions/27#discussioncomment-4317840
}
function addConfig() {
let c = {};
fill_config_if_missing(c);
build_config.push(c);
return c;
}
// returns "true" if config was altered!
function fill_config_if_missing(config) {
let changed = false;
if (!("id" in config)) {
let id = rand_id();
let unique = true;
do {
for (let i=0; i<build_config.length; i+=1) {
if (build_config[i] && build_config[i].id && build_config[i].id == id) {
unique = false;
break;
}
}
} while (unique == false)
config["id"] = id;
changed = true;
}
// manual clone...
for (let k in build_config_default) {
if (!(k in config)) {
config[k] = build_config_default[k];
changed = true;
}
}
// We omit the check here to ensure that config is restricted to the same properties as build_config_default.
// Perhaps at one time someone tries to patch in functionality ... and might consider this helpful!
return changed;
}
let mcu_plugin_config = {
"platforms": [],
"ports": [],
"experimental": 0
};
/******
* Handling class for the progress bar div
******/
class ProgressBar {
#bar;
#state;
#width;
constructor(el) {
this.#bar = el;
this.#state = true;
this.#width = 0;
}
reset() {
this.#state = true;
this.set(0);
}
stop() {
this.set(0);
this.#state = false;
}
ping() {
if (this.#width < 100) {
this.set(this.#width >= 15 ? 1 : this.#width + 1);
}
}
set(percent) {
this.#width = percent;
if (this.#state === true) {
let bar = $(this.#bar);
bar.css({"width": `${this.#width}%`});
}
}
}
RED.editor.registerEditPane("editor-tab-manifest", function(node) {
return {
label: "MCU: manifest.json",
name: "manifest.json",
iconClass: "fa fa-microchip",
create: function(container) {
this.editor = buildManifestForm(container,node);
},
resize: function(size) {
this.editor.resize();
},
close: function() {
this.editor.destroy();
this.editor = null;
},
show: function() {
this.editor.focus();
},
apply: function(editState) {
let new_manifest = this.editor.getValue();
if (node._mcu?.manifest) {
// Has existing manifest property
if (new_manifest.trim() === "") {
// New value is blank - remove the property
editState.changed = true;
editState.changes._mcu = structuredClone(node._mcu);
delete node._mcu.manifest;
} else if (node._mcu.manifest !== new_manifest) {
// New value is different
editState.changed = true;
editState.changes._mcu = structuredClone(node._mcu);
node._mcu.manifest = new_manifest;
}
} else {
// No existing manifest
if (new_manifest.trim() !== "") {
editState.changed = true;
editState.changes._mcu = structuredClone(node._mcu);
node._mcu.manifest = new_manifest;
}
}
}
};
}, function(node) {
return (node.type === "tab" && node._mcu?.mcu === true);
});
function buildManifestForm(container,node) {
var dialogForm = $('<form class="dialog-form form-horizontal" autocomplete="off"></form>').appendTo(container);
var toolbarRow = $('<div></div>').appendTo(dialogForm);
var row = $('<div class="form-row node-text-editor-row" style="position:relative; padding-top: 4px; height: 100%"></div>').appendTo(dialogForm);
var editorId = "node-info-input-info-editor-"+Math.floor(1000*Math.random());
$('<div style="height: 100%" class="node-text-editor" id="'+editorId+'" ></div>').appendTo(row);
var nodeManifestEditor = RED.editor.createEditor({
id: editorId,
mode: 'ace/mode/json',
stateId: RED.editor.generateViewStateId("node", node, "manifest"),
value: node._mcu?.manifest || ""
});
node.manifest = nodeManifestEditor;
return nodeManifestEditor;
}
RED.plugins.registerPlugin("node-red-mcu", {
onadd: function() {
// We use this to check if the runtime side of node-re-mcu-plugin
// is up & running. If not, we'll cancel registerPlugin!
new Promise((resolve, reject) => {
$.get({
url: "mcu/config/plugin",
contentType: "application/json",
success: function (response) {
resolve(response);
},
error: function (error) {
reject(error);
}
});
})
.then((data) => {
let incoming_config; // {targets: [{...}]}
try {
incoming_config = JSON.parse(data);
} catch (err) {
console.error("node-red-mcu-plugin: Failed to parse JSON config data. Setup canceled.")
return;
}
for (key in mcu_plugin_config) {
if (key in incoming_config) {
mcu_plugin_config[key] = incoming_config[key];
}
}
try {
_onAdd(mcu_plugin_config.experimental);
}
catch(error) {
console.error(error);
}
})
.catch((error) => {
console.log(error);
console.error("node-red-mcu-plugin: Runtime module not responding. Setup canceled.")
})
return;
},
onremove: function() {
// this is incomplete!
RED.events.off("flows:add", onFlowAdd)
RED.events.off("flows:remove", onObjectRemove)
RED.events.off("flows:change", onFlowChange)
RED.events.off("flows:reorder", onFlowsReorder)
RED.sidebar.removeTab("node-red-mcu");
}
})
function _onAdd(MCU_EXPERIMENTAL) {
MCU_EXPERIMENTAL ??= 0;
RED.events.on("flows:add", onFlowAdd)
RED.events.on("flows:remove", onObjectRemove)
RED.events.on("flows:change", onFlowChange)
RED.events.on("flows:reorder", onFlowsReorder)
RED.events.on("workspace:clear", () => {
objects = {};
flowData = [];
if (flowSelector) {
flowSelector.treeList('clearSelection');
flowSelector.treeList('empty');
}
})
progress_bar = new ProgressBar(".mcu-build-progress-bar");
const progress_re = /^(?:Writing at 0x)(?:[0123456789abcdef]+\.{3} \()(\d{1,3})(?: %\))/;
// Flag set when '......' printing happens;
// this ensures that a line feed is inserted afterwards - if necessary!
let waiting_dot = 0;
let mcu_console = {
log(msg) {
let mcuc = $("#mcu_console");
// If the textarea is initially scrolled to its bottom position...
let at_the_bottom = (mcuc[0].scrollHeight - mcuc.scrollTop() - mcuc[0].clientHeight < 8);
if (msg.length > 0) {
if (msg == "__flash_console") {
flashTab("__console__")
} else {
let text = mcuc.val()
// console.log(msg, msg.length, msg.charCodeAt(0));
if ("." === msg) {
waiting_dot += 1;
} else if (waiting_dot > 1 && msg[0] !== "." && msg.charCodeAt(0) !== 10) {
text += '\n';
waiting_dot = 0;
} else if ("\r\n" === msg || "\n\r" === msg) {
msg = "\n";
} else {
waiting_dot = 0;
}
text += msg;
mcuc.val(text);
if (at_the_bottom) {
// ... we scroll it there afterwards again.
mcuc.scrollTop(mcuc[0].scrollHeight);
}
res = progress_re.exec(msg);
if (res && res.length > 1) {
progress_bar.set(res[1]);
} else {
progress_bar.ping();
}
}
}
}
}
RED.comms.subscribe("mcu/stdout/#", function (topic, msg) {
mcu_console.log(msg);
});
RED.comms.subscribe("mcu/serialports", function (topic, msg) {
let ports = mcu_plugin_config.ports;
let changed = false;
for (let i = 0; i < msg.length; i += 1) {
if (ports.length < i - 1) {
ports.push(msg[i]);
changed = true;
} else if (ports[i] !== msg[i]) {
ports[i] = msg[i];
changed = true;
}
}
if (msg.length < ports.length) {
ports.length = msg.length;
changed = true;
}
if (changed) {
RED.events.emit("mcu:validate-ports");
}
})
RED.comms.subscribe("mcu/notify", function (topic, data) {
progress_bar.reset();
if (data) {
// sanitize data
// ToDo: Confirm if this is necessary!
if (data.options?.buttons)
delete data.options.buttons
RED.notify(data.message, data.options);
}
})
let flows2build = [];
// Patch mcu flag into the defaults of any type
// This is a prerequisite as the NR editor only forwards properties to the runtime,
// that are defined in the defaults.
function patch_node_type_for_mcu(nt) {
let t = RED.nodes.getType(nt);
if (!t) return;
t.defaults["_mcu"] = { value: {"mcu": false} }
// Patch the onadd function of nodes to take over the flow's _mcu status
if (nt !== "tab") {
let onadd_orig = t.onadd;
t.onadd = function() {
let n = this;
if (n && n.type !== "tab" && n.z) {
let ws = RED.nodes.workspace(n.z);
let _mcu = n._mcu ?? {};
_mcu.mcu = (RED.nodes.workspace(n.z)?._mcu?.mcu === true) ?? false;
n._mcu = _mcu;
}
if (!onadd_orig) return;
return onadd_orig.call(n);
}
}
t = RED.nodes.getType(nt);
if (!t.defaults._mcu) {
console.log("MCU: Failed to patch type '" + nt + "'.")
}
}
// First for the nodes...
RED.events.on("registry:node-type-added", function (nt) {
patch_node_type_for_mcu(nt);
});
// Same for the flow...
patch_node_type_for_mcu("tab");
content = document.createElement("div");
content.className = "red-ui-sidebar-info"
let stackContainer = $("<div>", { class: "red-ui-sidebar-info-stack" }).appendTo(content);
selectorPanel = $("<div>").css({
"overflow": "hidden",
"height": "calc(70%)"
}).appendTo(stackContainer);
let selectorPanelHeader = $("<div>", { class: "red-ui-sidebar-header" }).css({ "text-align": "left", display: "flex" }).appendTo(selectorPanel);
let selectorPanelHeaderLeft = $('<span>').css({ "flex-grow": "1", "text-align": "left" }).appendTo(selectorPanelHeader);
let selectorPanelHeaderRight = $('<span>').css({ "display": "unset" }).appendTo(selectorPanelHeader);
$("<span>").text("Flows to build for MCU").css({ width: "100%", "text-align": "left", "font-weight": "bold" }).appendTo(selectorPanelHeaderLeft);
let selectorPanelInfo = $('<span>').appendTo(selectorPanelHeaderRight);
$('<i class="fa fa-info-circle fa-lg" aria-hidden="true"></i>').css({'color': 'rgb(7, 48, 112)'}).appendTo(selectorPanelInfo);
let selectorPanelInfoTip = $('<span><span>When deployed, flows selected here are in<br><b>stand-by mode</b>, awaiting an incoming <br>MCU connection.<br>De-select them & deploy again to enable<br>standard Node-RED functionality.</span>');
let selectorPanelInfoTooltip = RED.popover.tooltip(selectorPanelInfo, selectorPanelInfoTip);
buildPanel = $("<div>").css({
"overflow": "hidden",
"height": "100%",
"min-height": "50px"
}).appendTo(stackContainer);
buildPanelHeader = $("<div>", { class: "red-ui-sidebar-header" }).css({ "text-align": "left" }).appendTo(buildPanel);
let buildPanelHeaderLeft = $('<span>').css({ "flex-grow": "1", "text-align": "left" }).appendTo(buildPanelHeader);
let buildPanelHeaderRight = $('<span>').css({ "display": "unset" }).appendTo(buildPanelHeader);
$("<span>").text("MCU Build Configurations").css({ "text-align": "left", "font-weight": "bold", "vertical-align": "middle", display: "inline-block" }).appendTo(buildPanelHeaderLeft);
tabsContainer = $('<div>', { class: "tabs-container" }).css({ "position": "absolute", "top": "calc(100% - 35px)", "width": "100%" }).appendTo(buildPanel);
let buildTabs = $('<ul>', { id: "tabs-tabs" }).appendTo(tabsContainer);
panels = RED.panels.create({
container: stackContainer,
resize: function (top, bottom) {
resize_sidebar(top, bottom);
}
})
function resize_panels() {
if (panels) {
var h = $(content).parent().height();
panels.resize(h);
}
}
RED.events.on("sidebar:resize", resize_panels);
$(window).on("resize", resize_panels);
$(window).on("focus", resize_panels);
// the flows treeList
flowSelector = $("<div>").css({ width: "100% - 8px", height: "calc(100% - 45px)", margin: "4px" }).appendTo(selectorPanel);
flowSelector.treeList({
data: flowData,
multi: true,
}).on('treelistselect', function (event, item) {
let selected = flowSelector.treeList("selected")
let f2b = [];
for (let ii = 0; ii < selected.length; ii += 1) {
let s = selected[ii];
f2b.push(s.id);
}
// TODO: This should be UNDOable!
let changed = false;
let f2bl = f2b.length;
RED.nodes.eachWorkspace(function (ws) {
if (typeof(ws._mcu) !== "object") {
delete ws._mcu;
}
ws._mcu = ws._mcu || { "mcu": false };
// c === 3: node has flag but need none; remove it
// c === 2: node got mcu flag
// c === 1: node already has flag
// c === 0: node has no flag nor needs one
let c = 0;
for (let i = 0; i < f2bl; i += 1) {
// mark the flow
if (ws.id && ws.id === f2b[i]) {
if (ws._mcu?.mcu === true) {
c = 1;
} else {
c = 2;
}
break;
}
}
if (c > 0) {
ws._mcu.mcu = true;
}
if (c < 1) {
if (ws._mcu.mcu === true) {
c = 3;
}
ws._mcu.mcu = false;
}
if (c > 1) {
ws.changed = true;
RED.events.emit("flows:change", ws);
changed = true;
}
})
RED.nodes.eachNode(function (node) {
if (typeof(node._mcu) !== "object") {
delete node._mcu;
}
node._mcu ??= { "mcu": false };
// c === 3: node has flag but need none; remove it
// c === 2: node got mcu flag
// c === 1: node already has flag
// c === 0: node has no flag nor needs one
let c = 0;
for (let i = 0; i < f2bl; i += 1) {
// mark the nodes
if (node.z && node.z === f2b[i]) {
if (node._mcu.mcu === true) {
c = 1;
} else {
c = 2;
}
break;
}
}
if (c > 0) {
node._mcu.mcu = true;
}
if (c < 1) {
if (node._mcu.mcu === true) {
c = 3;
};
node._mcu.mcu = false;
}
if (c > 1) {
node.changed = true;
RED.events.emit("nodes:change", node);
changed = true;
}
})
if (changed) {
RED.nodes.dirty(true);
RED.view.updateActive();
RED.view.redraw(true);
let now = new Date().getTime() / 1000;
if (now - selectorPanelTipStamp > 300) {
selectorPanelInfoTooltip.open();
selectorPanelTipStamp = now;
setTimeout(function() {
selectorPanelInfoTooltip.close()
}, 7500);
}
}
flows2build = f2b;
});
RED.sidebar.addTab({
id: "node-red-mcu",
label: "MCU",
name: "Node-Red MCU",
iconClass: "fa fa-microchip",
content: content,
// toolbar: footerToolbar,
enableOnEdit: true,
// action: "core:show-flow-debugger-tab"
onchange: function() {
setTimeout(resize_sidebar, 0);
}
});
let console_container;
let config_container;
let config_scroll_top;
tabs = RED.tabs.create({
id: "tabs-tabs",
vertical: false,
scrollable: true,
onchange: function (tab) {
if (console_container) {
if (tab.id == "__console__") {
console_container.show();
} else {
console_container.hide();
}
}
if (config_container) {
if (tab.id != "__console__") {
config_container.show();
if (config_view) {
setTimeout(() => {
config_view.parent().scrollTop(config_scroll_top);
}, 0);
}
} else {
config_scroll_top = config_view?.parent()?.scrollTop() ?? 0;
config_container.hide();
}
}
// this here is for the editableList
resize_sidebar()
}
});
// patch for bottom placed tabs
tabsContainer.find(".red-ui-tabs").addClass("red-ui-tabs-bottom")
tabsContainer.find(".red-ui-tab-button").addClass("red-ui-tabs-bottom")
let tab = {
id: "1",
label: "Configuration",
iconClass: "fa fa-cog",
}
tabs.addTab(tab);
let tab2 = {
id: "__console__",
label: "Console Monitor",
iconClass: "fa fa-window-maximize",
}
tabs.addTab(tab2);
tabs.resize();
bPHh = buildPanelHeader.height();
console_container = $('<div>').css({ "height": "calc(100% - 42px - 40px - 4px)", "display": "none" }).appendTo(buildPanel);
console_view = $('<textarea id="mcu_console" wrap="hard" readonly>')
.css({
"position": "relative",
"box-sizing": "border-box",
"margin-top": "4px",
"margin-left": "4px",
"margin-right": "4px",
"margin-bottom": "2px",
"font-family": "monospace",
"font-size": "9pt",
"color": "white",
"background-color": "black",
"white-space": "pre",
"overflow": "scroll",
"width": "calc(100% - 8px)",
"resize": "none",
"line-height": "normal",
"cursor": "default",
"height": "100%",
"min-height": "89px",
"scrollbar-width": "none"
})
.appendTo(console_container)
.hover(
function() {
if (ccbt) {
clearTimeout(ccbt);
}
ccbt = setTimeout(function () {
let v = console_view.val();
if (v && typeof (v) == "string" && v.length > 0) {
if (console_copy_button) {
console_copy_button.show(200);
}
}
}, 200);
},
function (evt) {
if (ccbt) {
clearTimeout(ccbt);
}
ccbt = setTimeout(function () {
let rect = evt.target.getBoundingClientRect();
let in_x = (evt.clientX > rect.left) && (evt.clientX < rect.right);
let in_y = (evt.clientY > rect.top) && (evt.clientY < rect.bottom);
if (in_x && in_y) {
return;
}
console_copy_button.hide(100);
}, 300);
}
);
$("<div class='mcu-build-progress-bar'>").appendTo(console_container);
let console_copy_button = $('<button class="red-ui-button" title="Test"><i class="fa fa-clipboard"></i></button>').appendTo(console_container);
let ccbt; // console_copy_button_timer
console_copy_button.css({
position: "absolute",
top: "calc(100% - 85px)",
left: "calc(100% - 46px)"
}).on("click", function(evt) {
if (!console_view) return;
let cp = console_view.val();
navigator.clipboard.writeText(cp);
RED.notify("Console data copied to Clipboard.", { type: "success", timeout: 2500 });
}).hide();
RED.popover.tooltip(console_copy_button,"Copy to Clipboard");
config_container = $('<div>')
.css({
"position": "absolute",
"height": "100%",
"margin": "4px",
"height": "calc(100% - 42px - 52px)",
"top": "51 px",
"box-sizing": "border-box",
"width": "calc(100% - 8px)"
})
.appendTo(buildPanel);
config_view = $('<ol id="__config_view__">').appendTo(config_container)
$("<div class='mcu-build-progress-bar'>").css({"margin-top": "4px", "margin-left": "0px"}).appendTo(config_container);
config_view.editableList({
addButton: "Add config...",
height: 200,
sortable: true,
removable: true,
addItem: function (container, index, data) {
let self = this;
if (isEmpty(data)) {
data = addConfig();
persist_config();
} else {
if (fill_config_if_missing(data)) {
persist_config();
}
}
container.parent().css({
"border-bottom": "1px solid rgb(7, 48, 112)"
});
// Patch onclick of the item-remove-button
// original @ editableList.js / 307
container.parent().find("a.red-ui-editableList-item-remove").unbind("click").on("click", function (evt) {
evt.preventDefault();
let that = self;
let row = container;
function del_config() {
let dt = row.data('data');
let li = row.parent();
li.addClass("red-ui-editableList-item-deleting")
li.fadeOut(300, function () {
$(this).remove();
if (that.removeItem) {
that.removeItem(dt);
}
});
}
let notify_delete = RED.notify("Delete this Build Configuration?", {
type: "compact",
modal: true,
fixed: true,
buttons: [
{
text: "No",
click: function (e) {
notify_delete.close();
return;
}
},
{
text: "Yes",
class: "primary",
click: function (e) {
notify_delete.close();
del_config();
return;
}
}
]
});
});
let initializing = true;
container.css({
overflow: 'hidden',
whiteSpace: 'nowrap',
display: "flex",
"align-items": "center",
});
let inputRows = $('<div></div>').appendTo(container);
let row1 = $('<div></div>').appendTo(inputRows);
// for later...
let platform;
let target = $('<input>').appendTo(row1)
target.typedInput({
type: "target",
types: [{
value: "target",
icon: "fa fa-bullseye",
options: [
{ value: "sim", label: "Simulator" },
{ value: "esp", label: "ESP8266 | Espressif" },
{ value: "esp32", label: "ESP32 | Espressif" },
{ value: "gecko", label: "Gecko | Silicon Labs" },
{ value: "nrf52", label: "nRF52 | Nordic" },
{ value: "pico", label: "Pico | Raspberry Pi" },
{ value: "qca4020", label: "QCA4020 | Qualcomm" },
],
}],
}).on('change', function (event, type, value) {
if (platform) {
platform.typedInput("types", [{
icon: "fa fa-at",
value: "platform",
options: mcu_plugin_config.platforms.filter(p => {
if (p.value == value) return true;
return (p.value.slice(0, value.length + 1) == value + "/")
}),
}]);
}
if (value == "sim") {
row2.hide(500);
if (instrumentation) {
let _instr = instrumentation.typedInput("value");
if (_instr.indexOf('d') < 0) {
instrumentation.typedInput("value", _instr === "i" ? "d,i" : "d")
data.debug = true;
persist_config();
}
}
} else if (value == "pico") {
row2.hide(500);
} else if (value == "nrf52") {
row2.hide(500);
} else {
row2.show(500);
}
});
$('<div>', { style: "width: 100%; height: 4px;" }).appendTo(row1);
platform = $('<input class="platform">').appendTo(row1);
platform.typedInput({
type: 'platform',
types: [
{
icon: "fa fa-at",
value: "platform",
options: mcu_plugin_config.platforms.filter(p => {
return (p.value.slice(0, 4) == "sim/")
}),
}]
})
.on('change', function (event, type, value) {
if (initializing === false) {
data.platform = value;
persist_config();
}
})
platform.next().css({ "min-width": "214px", "width": "auto" });
let row2 = $('<div>').css({ "margin-top": "4px" }).appendTo(inputRows).hide();
let plug = $('<input>').appendTo(row2);
plug.typedInput({
type: 'plug',
types: [{
icon: "fa fa-plug",
value: "plug",
hasValue: "true",
autoComplete: function (value, done) {
ret = [];
for (i = 0; i < mcu_plugin_config.ports.length; i += 1) {
ret.push({
value: mcu_plugin_config.ports[i],
label: mcu_plugin_config.ports[i],
"i": i
})
}
done(ret);
},
validate: function (value) {
for (i = 0; i < mcu_plugin_config.ports.length; i += 1) {
if (value == mcu_plugin_config.ports[i]) {
return true;
}
}
return false;
}
}],
}).on('change', function (event, type, value) {
if (initializing === false) {
data.port = value;
persist_config();
}
});
// rather than trying to make the input field autogrow, we set a fixed - most probably large enough - min-width here.
plug.next().css({ "min-width": "270px", "width": "auto" });
RED.events.on("mcu:validate-ports", function () {
plug.typedInput("validate");
});
let row3 = $('<div>').css({ "margin-top": "4px" }).appendTo(inputRows);
let bb = $('<span class="button-group">').appendTo(row3);
let bbb = $('<a id="mcu-plugin-build" href="#">').appendTo(bb);
bbb.button({
label: '<i class="fa fa-cog" style="margin-right: 10px"></i><span>Build</span>'
});
bbb.css({
background:"rgb(7, 48, 112)",
color: "white",
border: "none",
"margin-right": "0px",
"line-height": "25px"
}).on("click", function() {
if (MCU_EXPERIMENTAL & 1) {
launch_build(data.buildmode == "1" ? "mod" : undefined);
} else {
launch_build();
}
})
let bbd = $('<a id="mcu-plugin-build-select" href="#">').appendTo(bb);
bbd.button({
label: '<i class="fa fa-caret-down"></i>'
});
bbd.css({
background:"rgb(7, 48, 112)",
color: "white",
border: "none",
"line-height": "25px",
padding: "0.4em 0.6em"
})
bbd.on("click", function (e) {
e.preventDefault();
let options = [
{
label: $(`<span>Reconnect to xsbug...</span>`),
disabled: RED.nodes.dirty(),
onselect: function () {
launch_build("reconnect");
}
}
]
if (MCU_EXPERIMENTAL & 1) {
if (data.buildmode == "1") {
options.push({
label: $(`<span>Build host...</span>`),
onselect: function () {
launch_build("host");
}
})
}
}
if (MCU_EXPERIMENTAL & 2) {
if (data.buildmode !== "1") {
options.push( {
label: $(`<span>Build then flash local...</span>`),
// disabled: RED.nodes.dirty(),
onselect: function () {
launch_build("flash_local");
}
});
}
}
let menu = RED.popover.menu({
options: options,
});
// Patch away the <a> anchor of disabled menu items
// Att: This doesn't work with <string> labels
for (let i = 0; i < options.length; i++) {
let o = options[i];
if (o.disabled === true && typeof (o.label) !== "string") {
let link = o.label.parent();
// remove the <a> tag
o.label.appendTo(link.parent());
link.remove();
// make it look nice
o.label.css({
"display": "block",
"padding": "4px 8px 4px 16px",
"line-height": "20px",
"color": "silver"
})
}
}
menu.show({
target: bbd,
align: "left",
offset: [bbd.outerWidth() - 2, -1]
})
})
function launch_build(mode) {
mode ??= "build";
let spin_timer;
progress_width = 0;
function build(data) {
if (console_view) {
console_view.val("");
}
// compile the build parameters
let options = {};
for (key in data) {
if (key === "more") continue
else if (["debug", "release", "platform"].includes(key)) {
options[key] = data[key];
}
else if (key in build_config_default) {
// only forward non-default values
// & UI support specific values (as they have non-zero like defaults)!
if (["cll", "dll", "tc", "px", "py"].includes(key)) {
let d = parseInt(data[key]);
if (!Number.isNaN(d)) {
options[key] = d;
}
} else if (data[key] !== build_config_default[key]) {
options[key] = data[key];
}
} else {
options[key] = data[key]
}
}
if (spin_timer) {
clearTimeout(spin_timer);
spin_timer = undefined;
}
flashTab("__console__", "attention", 1000);
bbb.find("i").addClass("fa-spin");
spin_timer = setTimeout(function() {
bbb.find("i").removeClass("fa-spin");
}, 5000);
progress_bar.reset();
if (mode == "flash_local") {
return flash_local(options);
} else if (mode == "remote") {
options._remote = true;
return flash_local(options);
}
$.ajax({
url: "mcu/build",
contentType: "application/json",
type: "POST",
data: JSON.stringify({
"mode": mode,
"options": options
}),
success: function (resp) {
if (spin_timer) {
clearTimeout(spin_timer);
spin_timer = undefined;
bbb.find("i").removeClass("fa-spin");
}
},
error: function (jqXHR, textStatus, errorThrown) {
flashTab("__console__");
if (spin_timer) {
clearTimeout(spin_timer);
spin_timer = undefined;
bbb.find("i").removeClass("fa-spin");
}
progress_bar.stop();
}
});
}
if (RED.nodes.dirty() === true) {
let notify_deploy = RED.notify("Deploy changes prior building for MCU?", {
type: "info",
modal: true,
fixed: true,
buttons: [
{
text: "No",
click: function (e) {
notify_deploy.close();
build(data);
return;
}
},
{
text: "Yes",