UNPKG

@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
<!-- /* 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",