diff --git a/flows.json b/flows.json new file mode 100644 index 0000000..8c47600 --- /dev/null +++ b/flows.json @@ -0,0 +1,1068 @@ +[ + { + "id": "b475cb7c37295110", + "type": "tab", + "label": "Xilica FR1 Dashboard", + "disabled": false, + "info": "" + }, + { + "id": "42887eae2f29d3f0", + "type": "tab", + "label": "Xilica FR1 Dashboard", + "disabled": false, + "info": "" + }, + { + "id": "ui_base_main", + "type": "ui-base", + "name": "Main UI", + "path": "/ui", + "appIcon": "", + "includeClientData": true, + "acceptsClientConfig": [ + "ui-control", + "ui-notification" + ], + "showPathInSidebar": false, + "headerContent": "dashboard", + "titleBarStyle": "fixed", + "showReconnectNotification": true, + "notificationDisplayTime": 5, + "showDisconnectNotification": true, + "allowInstall": true + }, + { + "id": "efd7cd1777464c3d", + "type": "ui-theme", + "name": "LAV THEME", + "colors": { + "surface": "#d7d6d6", + "primary": "#5c727a", + "bgPage": "#eeeeee", + "groupBg": "#ffffff", + "groupOutline": "#cccccc" + }, + "sizes": { + "density": "default", + "pagePadding": "12px", + "groupGap": "12px", + "groupBorderRadius": "4px", + "widgetGap": "12px" + } + }, + { + "id": "ui_page_solaro", + "type": "ui-page", + "name": "Solaro FR1", + "ui": "ui_base_main", + "path": "", + "icon": "volume-high", + "layout": "grid", + "theme": "efd7cd1777464c3d", + "breakpoints": [ + { + "name": "Default", + "px": "0", + "cols": "3" + }, + { + "name": "Tablet", + "px": "576", + "cols": "6" + }, + { + "name": "Small Desktop", + "px": "768", + "cols": "9" + }, + { + "name": "Desktop", + "px": "1024", + "cols": "12" + } + ], + "order": 1, + "className": "", + "visible": true, + "disabled": false + }, + { + "id": "ui_group_master", + "type": "ui-group", + "name": "Master Gain", + "page": "ui_page_solaro", + "width": "4", + "height": "8", + "order": 2, + "showTitle": true, + "className": "", + "visible": "true", + "disabled": "false", + "groupType": "default" + }, + { + "id": "959e129052ea58f8", + "type": "ui-page", + "name": "Network", + "ui": "ui_base_main", + "path": "/network", + "icon": "wifi", + "layout": "grid", + "theme": "efd7cd1777464c3d", + "breakpoints": [ + { + "name": "Default", + "px": "0", + "cols": "3" + }, + { + "name": "Tablet", + "px": "576", + "cols": "6" + }, + { + "name": "Small Desktop", + "px": "768", + "cols": "9" + }, + { + "name": "Desktop", + "px": "1024", + "cols": "12" + } + ], + "order": 2, + "className": "", + "visible": true, + "disabled": false + }, + { + "id": "e46886701344918d", + "type": "global-config", + "env": [] + }, + { + "id": "c1fc81e5a697e224", + "type": "ui-group", + "name": "Wi-Fi", + "page": "959e129052ea58f8", + "width": "11", + "height": "11", + "order": 1, + "showTitle": true, + "className": "", + "visible": "true", + "disabled": "false", + "groupType": "default" + }, + { + "id": "e3b7d0b0ef7a4a01", + "type": "ui-page", + "name": "Solaro Channels", + "ui": "ui_base_main", + "path": "/channels", + "icon": "view-dashboard-outline", + "layout": "tabs", + "theme": "efd7cd1777464c3d", + "breakpoints": [ + { + "name": "Default", + "px": "0", + "cols": "3" + }, + { + "name": "Tablet", + "px": "576", + "cols": "6" + }, + { + "name": "Small Desktop", + "px": "768", + "cols": "9" + }, + { + "name": "Desktop", + "px": "1024", + "cols": "12" + } + ], + "order": 3, + "className": "", + "visible": "true", + "disabled": "false" + }, + { + "id": "e7bb2d8c0e0e2312", + "type": "ui-group", + "name": "Full Range", + "page": "e3b7d0b0ef7a4a01", + "width": "12", + "height": "8", + "order": 2, + "showTitle": true, + "className": "", + "visible": "true", + "disabled": "false", + "groupType": "default" + }, + { + "id": "ee3561b1b7a5a913", + "type": "ui-group", + "name": "Subwoofers", + "page": "e3b7d0b0ef7a4a01", + "width": "12", + "height": "8", + "order": 3, + "showTitle": true, + "className": "", + "visible": "true", + "disabled": "false", + "groupType": "default" + }, + { + "id": "6f58b0a32d6db114", + "type": "ui-group", + "name": "Right Side", + "page": "e3b7d0b0ef7a4a01", + "width": "4", + "height": "8", + "order": 4, + "showTitle": true, + "className": "", + "visible": "true", + "disabled": "false", + "groupType": "default" + }, + { + "id": "d0b6f2a2e4f2c211", + "type": "ui-group", + "name": "Left Side", + "page": "e3b7d0b0ef7a4a01", + "width": "4", + "height": "8", + "order": 1, + "showTitle": true, + "className": "", + "visible": "true", + "disabled": "false", + "groupType": "default" + }, + { + "id": "ab164b2f3eebf7d2", + "type": "ui-group", + "name": "Meters", + "page": "ui_page_solaro", + "width": "6", + "height": "6", + "order": 1, + "showTitle": true, + "className": "", + "visible": "true", + "disabled": "false", + "groupType": "default" + }, + { + "id": "99314ffcd1e195f5", + "type": "ui-text", + "z": "42887eae2f29d3f0", + "group": "ui_group_master", + "order": 2, + "width": "", + "height": "", + "name": "Current (dB)", + "label": "Current (dB)", + "layout": "row-spread", + "style": false, + "font": "", + "fontSize": "", + "color": "#000000", + "wrapText": false, + "className": "", + "value": "payload", + "valueType": "msg", + "x": 1430, + "y": 280, + "wires": [] + }, + { + "id": "b6e3985ddcab4c91", + "type": "ui-slider", + "z": "42887eae2f29d3f0", + "group": "ui_group_master", + "name": "MASTER_GAIN (dB)", + "label": "MASTER_GAIN (dB)", + "order": 1, + "width": "2", + "height": "8", + "passthru": false, + "outs": "all", + "topic": "", + "topicType": "str", + "thumbLabel": "true", + "showTicks": "false", + "min": -100, + "max": -18, + "step": 0.5, + "className": "", + "iconPrepend": "", + "iconAppend": "", + "color": "grey", + "colorTrack": "blue", + "colorThumb": "black", + "showTextField": false, + "x": 200, + "y": 300, + "wires": [ + [ + "2979ab7ac78ee6fb" + ] + ] + }, + { + "id": "2979ab7ac78ee6fb", + "type": "function", + "z": "42887eae2f29d3f0", + "name": "build SET MASTER_GAIN ", + "func": "const v = Number(msg.payload);\n// show value on the slider label while sending\nnode.status({text: v.toFixed(1)+\" dB\"});\nmsg.payload = `SET MASTER_GAIN ${v}`;\nreturn msg;", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 450, + "y": 300, + "wires": [ + [ + "f2a2b33c99f73ef4" + ] + ] + }, + { + "id": "deb330478d545760", + "type": "inject", + "z": "42887eae2f29d3f0", + "name": "poll GET", + "props": [ + { + "p": "payload" + } + ], + "repeat": "0.1", + "crontab": "", + "once": true, + "onceDelay": "0.5", + "topic": "", + "payload": "GET MASTER_GAIN", + "payloadType": "str", + "x": 120, + "y": 360, + "wires": [ + [ + "f2a2b33c99f73ef4" + ] + ] + }, + { + "id": "f2a2b33c99f73ef4", + "type": "function", + "z": "42887eae2f29d3f0", + "name": "append \\r", + "func": "if (typeof msg.payload !== 'string') msg.payload = String(msg.payload);\nmsg.payload += \"\\r\";\nreturn msg;", + "outputs": 1, + "x": 680, + "y": 340, + "wires": [ + [ + "b025382cef8ddeae" + ] + ] + }, + { + "id": "b025382cef8ddeae", + "type": "tcp request", + "z": "42887eae2f29d3f0", + "name": "FR1 192.168.1.244:10007", + "server": "192.168.1.244", + "port": "10007", + "out": "sit", + "ret": "string", + "splitc": " ", + "newline": "", + "trim": false, + "tls": "", + "x": 910, + "y": 400, + "wires": [ + [ + "d8273409d003853b", + "eee5fc7d794d0a80", + "eaf29f8c14a08046", + "93a6e10d2495c341" + ] + ] + }, + { + "id": "eee5fc7d794d0a80", + "type": "function", + "z": "42887eae2f29d3f0", + "name": "parse GET reply → number", + "func": "// Expected: MASTER_GAIN=-6.0\\r or OK\\r after SET\nconst s = (msg.payload||\"\").trim();\nif (/^MASTER_GAIN\\s*=/.test(s)) {\n const m = s.match(/=\\s*([-+]?\\d+(?:\\.\\d+)?)/);\n if (m) {\n const val = Number(m[1]);\n // update text and slider\n node.send([{payload: val.toFixed(1)}, {payload: val}]);\n }\n}\nreturn null; // outputs handled via node.send", + "outputs": 2, + "x": 1220, + "y": 320, + "wires": [ + [ + "99314ffcd1e195f5" + ], + [ + "56f258d24535e171" + ] + ] + }, + { + "id": "d8273409d003853b", + "type": "debug", + "z": "42887eae2f29d3f0", + "name": "Raw replies", + "active": false, + "tosidebar": true, + "complete": "payload", + "statusVal": "", + "statusType": "auto", + "x": 1170, + "y": 360, + "wires": [] + }, + { + "id": "56f258d24535e171", + "type": "link out", + "z": "42887eae2f29d3f0", + "name": "link out 2", + "mode": "link", + "links": [ + "95de5752c6e3da4b" + ], + "x": 1385, + "y": 340, + "wires": [] + }, + { + "id": "95de5752c6e3da4b", + "type": "link in", + "z": "42887eae2f29d3f0", + "name": "link in 2", + "links": [ + "56f258d24535e171" + ], + "x": 55, + "y": 300, + "wires": [ + [ + "b6e3985ddcab4c91" + ] + ] + }, + { + "id": "739b4b01aa486c67", + "type": "inject", + "z": "42887eae2f29d3f0", + "name": "poll VU_MASTER", + "props": [ + { + "p": "payload" + } + ], + "repeat": "0.2", + "crontab": "", + "once": true, + "onceDelay": "0.5", + "topic": "", + "payload": "GET VU_MASTER", + "payloadType": "str", + "x": 150, + "y": 400, + "wires": [ + [ + "749a672579e69106" + ] + ] + }, + { + "id": "eaf29f8c14a08046", + "type": "function", + "z": "42887eae2f29d3f0", + "name": "parse VU_MASTER replies", + "func": "const s = String(msg.payload || '').trim();\nconst m = s.match(/^VU[_\\s]?MASTER\\s*=\\s*([-+]?\\d+(?:\\.\\d+)?)/i);\nif (!m) return null;\nmsg.payload = Number(m[1]); // e.g. -28.7\nreturn msg;\n", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1220, + "y": 400, + "wires": [ + [ + "f7d60274be119ba9" + ] + ] + }, + { + "id": "749a672579e69106", + "type": "function", + "z": "42887eae2f29d3f0", + "name": "append \\r", + "func": "if (typeof msg.payload !== 'string') msg.payload = String(msg.payload);\nmsg.payload += \"\\r\";\nreturn msg;", + "outputs": 1, + "x": 320, + "y": 400, + "wires": [ + [ + "b025382cef8ddeae" + ] + ] + }, + { + "id": "22cd982ac31a676b", + "type": "debug", + "z": "42887eae2f29d3f0", + "name": "Template OUT", + "active": false, + "tosidebar": true, + "complete": "payload", + "statusVal": "", + "statusType": "auto", + "x": 1600, + "y": 400, + "wires": [] + }, + { + "id": "f7d60274be119ba9", + "type": "ui-template", + "z": "42887eae2f29d3f0", + "group": "ab164b2f3eebf7d2", + "page": "", + "ui": "", + "name": "vu_master", + "order": 1, + "width": "2", + "height": "8", + "head": "", + "format": "\n\n\n\n\n", + "storeOutMessages": true, + "passthru": true, + "resendOnRefresh": true, + "templateScope": "local", + "className": "", + "x": 1430, + "y": 400, + "wires": [ + [ + "22cd982ac31a676b" + ] + ] + }, + { + "id": "98fcc2746ef99725", + "type": "inject", + "z": "42887eae2f29d3f0", + "name": "Poll CH4/5/6 (200ms)", + "props": [ + { + "p": "payload" + } + ], + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": "0.5", + "topic": "", + "payload": "", + "payloadType": "str", + "x": 160, + "y": 520, + "wires": [ + [ + "831afc4f6e643a54" + ] + ] + }, + { + "id": "831afc4f6e643a54", + "type": "function", + "z": "42887eae2f29d3f0", + "name": "Round-robin GET CH4/5/6", + "func": "// Build a single multi-line SUBSCRIBE message for CH4OUT–CH28OUT\n// Each subscription is sent via TCP to Solaro\n\nconst lines = [];\n\n// Set update rate to 100ms\nlines.push('INTERVAL 100');\n\n// Generate all channel names dynamically (CH4OUT → CH28OUT)\nfor (let i = 4; i <= 28; i++) {\n lines.push(`SUBSCRIBE CH${i}OUT \"TCP\"`);\n}\nfor (let i = 1; i <= 4; i++) {\n lines.push(`SUBSCRIBE CH${i}_FROUT \"TCP\"`);\n}\nfor (let i = 25; i <= 28; i++) {\n lines.push(`SUBSCRIBE CH${i}_FROUT \"TCP\"`);\n}\nfor (let i = 1; i <= 4; i++) {\n lines.push(`SUBSCRIBE CH${i}_SUBOUT \"TCP\"`);\n}\nfor (let i = 25; i <= 28; i++) {\n lines.push(`SUBSCRIBE CH${i}_SUBOUT \"TCP\"`);\n}\n// Join commands with CR and append final CR (required by Solaro API)\nmsg.payload = lines.join('\\r') + '\\r';\n\nreturn msg;\n", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 400, + "y": 520, + "wires": [ + [ + "037d562b5e1e1b78" + ] + ] + }, + { + "id": "037d562b5e1e1b78", + "type": "function", + "z": "42887eae2f29d3f0", + "name": "append \\r", + "func": "if (typeof msg.payload !== 'string') msg.payload = String(msg.payload);\nmsg.payload += \"\\r\";\nreturn msg;", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 600, + "y": 520, + "wires": [ + [ + "e5b7326c3b0a69e3", + "b025382cef8ddeae" + ] + ] + }, + { + "id": "59dbb335713fd5f0", + "type": "rbe", + "z": "42887eae2f29d3f0", + "name": "Only on change per channel", + "func": "rbe", + "gap": "", + "start": "", + "inout": "out", + "septopics": true, + "property": "payload.state", + "topi": "topic", + "x": 960, + "y": 580, + "wires": [ + [ + "a7281d90be3924f1" + ] + ] + }, + { + "id": "c728f8b711b33b49", + "type": "udp out", + "z": "42887eae2f29d3f0", + "name": "OSC → CueCore2 10.2.71.130:8000", + "addr": "10.2.71.130", + "iface": "", + "port": "8000", + "ipv": "udp4", + "outport": "", + "base64": false, + "x": 1630, + "y": 580, + "wires": [] + }, + { + "id": "a7281d90be3924f1", + "type": "function", + "z": "42887eae2f29d3f0", + "name": "Encode OSC /position/ int ", + "func": "// Remember which channel was last ON\nlet lastOn = context.get('lastOn') || null;\n\nconst { idx, state } = msg.payload;\n\n// Only act when something changes\nif (state === 1) {\n // If a new channel turns ON\n if (lastOn !== idx) {\n // Update context\n context.set('lastOn', idx);\n\n // Encode OSC message for this channel\n function pad4(buf) {\n const pad = (4 - (buf.length % 4)) % 4;\n return Buffer.concat([buf, Buffer.alloc(pad)]);\n }\n\n function oscString(s) {\n return pad4(\n Buffer.concat([\n Buffer.from(s, 'utf8'),\n Buffer.from([0])\n ])\n );\n }\n\n const addr = oscString(`/position/${idx}`);\n const types = oscString(',i');\n const arg = Buffer.alloc(4);\n arg.writeInt32BE(1, 0); // ON\n\n msg.payload = Buffer.concat([addr, types, arg]);\n node.status({ text: `ON → position ${idx}` });\n return msg;\n } else {\n // Same channel turned on again → ignore\n return null;\n }\n} else if (state === 0 && lastOn === idx) {\n // If the currently active one turned off → reset\n context.set('lastOn', null);\n node.status({ text: `OFF → position ${idx}` });\n}\n\nreturn null;\n", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1260, + "y": 580, + "wires": [ + [ + "e72e32cf5af0e00f", + "c728f8b711b33b49" + ] + ] + }, + { + "id": "f24442d61d44086a", + "type": "debug", + "z": "42887eae2f29d3f0", + "name": "Parsed {idx,state}", + "active": true, + "tosidebar": true, + "complete": "payload", + "statusVal": "", + "statusType": "auto", + "x": 1550, + "y": 480, + "wires": [] + }, + { + "id": "e72e32cf5af0e00f", + "type": "debug", + "z": "42887eae2f29d3f0", + "name": "OSC bytes", + "active": true, + "tosidebar": true, + "complete": "payload", + "x": 1570, + "y": 520, + "wires": [] + }, + { + "id": "e5b7326c3b0a69e3", + "type": "debug", + "z": "42887eae2f29d3f0", + "name": "FR1 raw", + "active": true, + "tosidebar": true, + "complete": "payload", + "x": 620, + "y": 460, + "wires": [] + }, + { + "id": "93a6e10d2495c341", + "type": "function", + "z": "42887eae2f29d3f0", + "name": "Parse CHxOUT=TRUE/0 → {idx,state}", + "func": "const s = String(msg.payload ?? '').trim();\n\n// Accepts #CH12OUT, #CH1_FROUT, #CH28_SUBOUT\nconst m = s.match(/^(#CH((?:\\d+)(?:_(?:FR|SUB))?)OUT)\\s*=\\s*(.*)$/i);\nif (!m) return null;\n\nconst name = m[1];\nconst raw = m[3];\n\n// Get the leading digits after #CH, even if there's _FR/_SUB\nconst idxMatch = name.match(/^#CH(\\d+)/i);\nconst idx = idxMatch ? Number(idxMatch[1]) : NaN;\n\nlet state;\nif (/^(TRUE|on)$/i.test(raw)) {\n state = 0;\n} else if (/^(FALSE|off)$/i.test(raw)) {\n state = 1;\n} else if (/^[-+]?\\d+(?:\\.\\d+)?$/.test(raw)) {\n state = Number(raw) !== 0 ? 1 : 0;\n} else {\n state = 0;\n}\n\nreturn {\n topic: name.toUpperCase(),\n payload: { idx, state }\n};\n", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1250, + "y": 480, + "wires": [ + [ + "f24442d61d44086a", + "59dbb335713fd5f0" + ] + ] + }, + { + "id": "689ea5b3b7e5b6fb", + "type": "ui-button", + "z": "42887eae2f29d3f0", + "group": "c1fc81e5a697e224", + "name": "Scan Wi-Fi", + "label": "Scan Wi-Fi", + "order": 1, + "width": "1", + "height": "1", + "emulateClick": false, + "className": "", + "icon": "", + "iconPosition": "left", + "payload": "", + "payloadType": "str", + "topic": "", + "topicType": "str", + "buttonColor": "", + "textColor": "", + "iconColor": "", + "enableClick": true, + "enablePointerdown": false, + "pointerdownPayload": "", + "pointerdownPayloadType": "str", + "enablePointerup": false, + "pointerupPayload": "", + "pointerupPayloadType": "str", + "x": 130, + "y": 740, + "wires": [ + [ + "e5a955dbcbd0b0f9" + ] + ] + }, + { + "id": "e5a955dbcbd0b0f9", + "type": "exec", + "z": "42887eae2f29d3f0", + "command": "bash", + "addpay": false, + "append": "-lc \"sudo -n nmcli -t -f SSID,SIGNAL,SECURITY dev wifi rescan >/dev/null 2>&1; sudo -n nmcli -t -f SSID,SIGNAL,SECURITY dev wifi list\"", + "useSpawn": "false", + "timer": "", + "winHide": false, + "name": "nmcli scan", + "x": 360, + "y": 750, + "wires": [ + [ + "c5625882bf1d9523", + "650fa075c7bd5ee4" + ], + [], + [ + "70623f49cca4d94e" + ] + ] + }, + { + "id": "c5625882bf1d9523", + "type": "function", + "z": "42887eae2f29d3f0", + "name": "parse nmcli → options", + "func": "// Input (stdout): \"SSID:SIGNAL:SECURITY\" lines\nconst lines = String(msg.payload || \"\")\n .split(/\\r?\\n/)\n .map(s => s.trim())\n .filter(Boolean);\n\nconst bestBySsid = new Map();\nfor (const line of lines) {\n const parts = line.split(\":\");\n const ssid = parts[0] || \"\"; // may be empty (hidden)\n const signal = Number(parts[1] || 0);\n const sec = parts.slice(2).join(\":\"); // SECURITY may contain colons\n\n const display = ssid || \"(hidden)\";\n const label = `${display} (${signal}%)${sec ? \" • \" + sec : \"\"}`;\n\n const prev = bestBySsid.get(ssid);\n if (!prev || signal > prev.signal) {\n bestBySsid.set(ssid, { label, value: ssid, signal });\n }\n}\n\nmsg.options = [...bestBySsid.values()].map(({ label, value }) => ({ label, value }));\nmsg.payload = null; // keep payload clean\nreturn msg;\n", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 570, + "y": 750, + "wires": [ + [ + "4c38affd4fc6f7fd" + ] + ] + }, + { + "id": "4c38affd4fc6f7fd", + "type": "ui-dropdown", + "z": "42887eae2f29d3f0", + "group": "c1fc81e5a697e224", + "name": "SSID", + "label": "SSID", + "tooltip": "", + "order": 2, + "width": 3, + "height": 1, + "passthru": true, + "multiple": false, + "chips": false, + "clearable": false, + "options": [], + "topic": "", + "topicType": "str", + "className": "", + "typeIsComboBox": true, + "msgTrigger": "onChange", + "x": 770, + "y": 750, + "wires": [ + [ + "9143c8d7cb5f0695" + ] + ] + }, + { + "id": "9143c8d7cb5f0695", + "type": "change", + "z": "42887eae2f29d3f0", + "name": "store ssid", + "rules": [ + { + "t": "set", + "p": "ssid", + "pt": "flow", + "to": "payload", + "tot": "msg" + } + ], + "x": 950, + "y": 750, + "wires": [ + [] + ] + }, + { + "id": "5d220b230ade6d6d", + "type": "ui-text-input", + "z": "42887eae2f29d3f0", + "group": "c1fc81e5a697e224", + "name": "Password", + "label": "Password", + "order": 3, + "width": 3, + "height": 1, + "topic": "", + "mode": "password", + "delay": "0", + "className": "", + "x": 1000, + "y": 800, + "wires": [ + [ + "1011308429ae7b27" + ] + ] + }, + { + "id": "1011308429ae7b27", + "type": "change", + "z": "42887eae2f29d3f0", + "name": "store pass (flow)", + "rules": [ + { + "t": "set", + "p": "pass", + "pt": "flow", + "to": "payload", + "tot": "msg" + } + ], + "x": 1200, + "y": 800, + "wires": [ + [] + ] + }, + { + "id": "814b0a944c467115", + "type": "ui-button", + "z": "42887eae2f29d3f0", + "group": "c1fc81e5a697e224", + "name": "Connect", + "label": "Connect", + "order": 4, + "width": "1", + "height": "1", + "emulateClick": false, + "className": "", + "icon": "", + "iconPosition": "left", + "payload": "", + "payloadType": "str", + "topic": "", + "topicType": "str", + "buttonColor": "", + "textColor": "", + "iconColor": "", + "enableClick": true, + "enablePointerdown": false, + "pointerdownPayload": "", + "pointerdownPayloadType": "str", + "enablePointerup": false, + "pointerupPayload": "", + "pointerupPayloadType": "str", + "x": 120, + "y": 900, + "wires": [ + [ + "7a4b562cba69efaa" + ] + ] + }, + { + "id": "7a4b562cba69efaa", + "type": "function", + "z": "42887eae2f29d3f0", + "name": "build nmcli cmd", + "func": "const ssid = flow.get('ssid');\nconst pass = flow.get('pass') || '';\nif (!ssid) return [null, { payload: 'Select an SSID first.' }];\n\nconst q = s => `'` + String(s).replace(/'/g, `'\\\\''`) + `'`; // safe single-quote\n\nlet inner = `/usr/bin/nmcli dev wifi connect ${q(ssid)}`;\nif (pass) inner += ` password ${q(pass)}`;\n\n// Exec node runs: sh -c \"\"\nconst cmd = `-c \"sudo -n ${inner}\"`;\n\nreturn [\n { payload: cmd }, // → Exec (runs: sh -c \"sudo -n /usr/bin/nmcli …\")\n { payload: `Connecting to ${ssid}...` } // → Status\n];\n", + "outputs": 2, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 330, + "y": 900, + "wires": [ + [ + "9f7621962705882b" + ], + [ + "f84f8a46913a2fef" + ] + ] + }, + { + "id": "9f7621962705882b", + "type": "exec", + "z": "42887eae2f29d3f0", + "command": "sh", + "addpay": "payload", + "append": "", + "useSpawn": "false", + "timer": "", + "winHide": false, + "name": "nmcli connect", + "x": 640, + "y": 860, + "wires": [ + [ + "c066f2a161e02498" + ], + [], + [ + "70623f49cca4d94e" + ] + ] + }, + { + "id": "c066f2a161e02498", + "type": "function", + "z": "42887eae2f29d3f0", + "name": "result → status", + "func": "const s = String(msg.payload||'');\nlet ok = /successfully activated|Device.*successfully activated/i.test(s);\nlet txt = ok ? 'Connected.' : s.trim() || 'Failed.';\nreturn { payload: txt };\n", + "outputs": 1, + "x": 710, + "y": 1000, + "wires": [ + [ + "f84f8a46913a2fef" + ] + ] + }, + { + "id": "f84f8a46913a2fef", + "type": "ui-text", + "z": "42887eae2f29d3f0", + "group": "c1fc81e5a697e224", + "order": 5, + "width": 6, + "height": 1, + "name": "Status", + "label": "Status", + "layout": "row-spread", + "style": false, + "font": "", + "fontSize": "", + "color": "#000000", + "wrapText": true, + "className": "", + "value": "payload", + "valueType": "msg", + "x": 870, + "y": 960, + "wires": [] + }, + { + "id": "70623f49cca4d94e", + "type": "debug", + "z": "42887eae2f29d3f0", + "name": "stderr", + "active": false, + "tosidebar": true, + "complete": "payload", + "x": 810, + "y": 800, + "wires": [] + }, + { + "id": "650fa075c7bd5ee4", + "type": "debug", + "z": "42887eae2f29d3f0", + "name": "nmcli scan debug", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 590, + "y": 660, + "wires": [] + } +] \ No newline at end of file diff --git a/imgs/19.png b/imgs/19.png new file mode 100755 index 0000000..8cb7fcd Binary files /dev/null and b/imgs/19.png differ diff --git a/imgs/21.png b/imgs/21.png new file mode 100755 index 0000000..f985cce Binary files /dev/null and b/imgs/21.png differ diff --git a/imgs/5.png b/imgs/5.png new file mode 100755 index 0000000..ec1a359 Binary files /dev/null and b/imgs/5.png differ diff --git a/imgs/6.png b/imgs/6.png new file mode 100755 index 0000000..b2e09eb Binary files /dev/null and b/imgs/6.png differ diff --git a/imgs/7.png b/imgs/7.png new file mode 100755 index 0000000..481b91e Binary files /dev/null and b/imgs/7.png differ