diff --git a/flows/showroom_flow.json b/flows/showroom_flow.json index 56aebda..e8c98ad 100644 --- a/flows/showroom_flow.json +++ b/flows/showroom_flow.json @@ -1,19 +1,57 @@ -ii[ +[ { - "id": "tabXilicaDash", + "id": "8bbc764cd051ef1b", "type": "tab", "label": "Xilica FR1 Dashboard", "disabled": false, "info": "" }, { - "id": "8ec2e59f8a7e993a", + "id": "a46fbefc8db2270b", + "type": "group", + "z": "8bbc764cd051ef1b", + "name": "CueCore2 Group", + "style": { + "label": true + }, + "nodes": [ + "4e52ab5c6cc5f16a", + "3c81e7f06c408f8a", + "af2d1ab8e73b9667", + "d4bb5ead71493ebe", + "66a230ec07338045" + ], + "x": 1114, + "y": 439, + "w": 1432, + "h": 208 + }, + { + "id": "66a230ec07338045", + "type": "group", + "z": "8bbc764cd051ef1b", + "g": "a46fbefc8db2270b", + "name": "CueCore 2 UI (device info)", + "style": { + "label": true + }, + "nodes": [ + "a7f24e2687a40804", + "91810af23bcdb612" + ], + "x": 1554, + "y": 539, + "w": 512, + "h": 82 + }, + { + "id": "e57cccd09cde445b", "type": "ui-text", - "z": "tabXilicaDash", + "z": "8bbc764cd051ef1b", "group": "ui_group_master", - "order": "", - "width": "", - "height": "", + "order": 3, + "width": "1", + "height": "1", "name": "Current (dB)", "label": "Current (dB)", "layout": "row-spread", @@ -25,20 +63,20 @@ ii[ "className": "", "value": "payload", "valueType": "msg", - "x": 1090, - "y": 400, + "x": 1470, + "y": 200, "wires": [] }, { - "id": "f64df68a17fafed8", + "id": "db4f60616c5bb903", "type": "ui-slider", - "z": "tabXilicaDash", + "z": "8bbc764cd051ef1b", "group": "ui_group_master", "name": "MASTER_GAIN (dB)", "label": "MASTER_GAIN (dB)", - "order": "", - "width": "1", - "height": "7", + "order": 2, + "width": "2", + "height": "8", "passthru": false, "outs": "all", "topic": "", @@ -55,18 +93,18 @@ ii[ "colorTrack": "blue", "colorThumb": "black", "showTextField": false, - "x": 540, + "x": 180, "y": 240, "wires": [ [ - "ebeeed016133d1b1" + "ab91656d7341e4d7" ] ] }, { - "id": "ebeeed016133d1b1", + "id": "ab91656d7341e4d7", "type": "function", - "z": "tabXilicaDash", + "z": "8bbc764cd051ef1b", "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, @@ -75,18 +113,18 @@ ii[ "initialize": "", "finalize": "", "libs": [], - "x": 850, + "x": 430, "y": 240, "wires": [ [ - "37a2739c39fb28c6" + "f08eb151bb70d817" ] ] }, { - "id": "ff5d04b8e8358ac7", + "id": "72a778b813cb0915", "type": "inject", - "z": "tabXilicaDash", + "z": "8bbc764cd051ef1b", "name": "poll GET", "props": [ { @@ -100,33 +138,33 @@ ii[ "topic": "", "payload": "GET MASTER_GAIN", "payloadType": "str", - "x": 480, - "y": 320, - "wires": [ - [ - "37a2739c39fb28c6" - ] - ] - }, - { - "id": "37a2739c39fb28c6", - "type": "function", - "z": "tabXilicaDash", - "name": "append \\r", - "func": "if (typeof msg.payload !== 'string') msg.payload = String(msg.payload);\nmsg.payload += \"\\r\";\nreturn msg;", - "outputs": 1, - "x": 740, + "x": 500, "y": 340, "wires": [ [ - "6f693811595db658" + "f08eb151bb70d817" ] ] }, { - "id": "6f693811595db658", + "id": "f08eb151bb70d817", + "type": "function", + "z": "8bbc764cd051ef1b", + "name": "append \\r", + "func": "if (typeof msg.payload !== 'string') msg.payload = String(msg.payload);\nmsg.payload += \"\\r\";\nreturn msg;", + "outputs": 1, + "x": 680, + "y": 280, + "wires": [ + [ + "366f5e1cbb7a0f46" + ] + ] + }, + { + "id": "366f5e1cbb7a0f46", "type": "tcp request", - "z": "tabXilicaDash", + "z": "8bbc764cd051ef1b", "name": "FR1 192.168.1.244:10007", "server": "192.168.1.244", "port": "10007", @@ -136,81 +174,87 @@ ii[ "newline": "", "trim": false, "tls": "", - "x": 550, + "x": 910, "y": 400, "wires": [ [ - "fd2ea6ae724caf88", - "2f4b662b4ed88da3", - "8d72b821fca250d0" + "c8fc4af3e7fdbb59", + "2141a20e5c74aa81", + "7f1ff26625a38e58", + "4e52ab5c6cc5f16a" ] ] }, { - "id": "2f4b662b4ed88da3", + "id": "2141a20e5c74aa81", "type": "function", - "z": "tabXilicaDash", + "z": "8bbc764cd051ef1b", "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", + "func": "\n\n// 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": 820, - "y": 400, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1220, + "y": 240, "wires": [ [ - "8ec2e59f8a7e993a" + "e57cccd09cde445b" ], [ - "8746aa1d3cf6fc69" + "dd021e3fbc30688d" ] ] }, { - "id": "fd2ea6ae724caf88", + "id": "c8fc4af3e7fdbb59", "type": "debug", - "z": "tabXilicaDash", + "z": "8bbc764cd051ef1b", "name": "Raw replies", "active": false, "tosidebar": true, "complete": "payload", "statusVal": "", "statusType": "auto", - "x": 850, - "y": 500, + "x": 1170, + "y": 280, "wires": [] }, { - "id": "8746aa1d3cf6fc69", + "id": "dd021e3fbc30688d", "type": "link out", - "z": "tabXilicaDash", + "z": "8bbc764cd051ef1b", "name": "link out 2", "mode": "link", "links": [ - "f267313c2d612dea" + "fb625579ecce6897" ], - "x": 985, - "y": 440, + "x": 1405, + "y": 280, "wires": [] }, { - "id": "f267313c2d612dea", + "id": "fb625579ecce6897", "type": "link in", - "z": "tabXilicaDash", + "z": "8bbc764cd051ef1b", "name": "link in 2", "links": [ - "8746aa1d3cf6fc69" + "dd021e3fbc30688d" ], - "x": 265, + "x": 35, "y": 240, "wires": [ [ - "f64df68a17fafed8" + "db4f60616c5bb903" ] ] }, { - "id": "c2fce4e36d067d34", + "id": "33a36424df3bc62c", "type": "inject", - "z": "tabXilicaDash", + "z": "8bbc764cd051ef1b", "name": "poll VU_MASTER", "props": [ { @@ -228,34 +272,34 @@ ii[ "y": 400, "wires": [ [ - "2100bc16276dbd8d" + "ca98de1bab349c37" ] ] }, { - "id": "8d72b821fca250d0", + "id": "7f1ff26625a38e58", "type": "function", - "z": "tabXilicaDash", + "z": "8bbc764cd051ef1b", "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", + "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]); \nreturn msg;\n", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], - "x": 600, - "y": 840, + "x": 1220, + "y": 320, "wires": [ [ - "a3665ff2cf1a93da" + "260885bbfa09d5c8" ] ] }, { - "id": "2100bc16276dbd8d", + "id": "ca98de1bab349c37", "type": "function", - "z": "tabXilicaDash", + "z": "8bbc764cd051ef1b", "name": "append \\r", "func": "if (typeof msg.payload !== 'string') msg.payload = String(msg.payload);\nmsg.payload += \"\\r\";\nreturn msg;", "outputs": 1, @@ -263,70 +307,251 @@ ii[ "y": 400, "wires": [ [ - "6f693811595db658" + "366f5e1cbb7a0f46" ] ] }, { - "id": "3d0401ed2bb481cd", - "type": "debug", - "z": "tabXilicaDash", - "name": "Template OUT", - "active": true, - "tosidebar": true, - "complete": "payload", - "statusVal": "", - "statusType": "auto", - "x": 1100, - "y": 840, - "wires": [] - }, - { - "id": "a3665ff2cf1a93da", + "id": "260885bbfa09d5c8", "type": "ui-template", - "z": "tabXilicaDash", - "group": "ui_group_meters", + "z": "8bbc764cd051ef1b", + "group": "ui_group_master", "page": "", "ui": "", "name": "vu_master", - "order": 0, - "width": 0, - "height": 0, + "order": 1, + "width": "2", + "height": "8", "head": "", - "format": "\n\n\n\n\n", + "format": "\n\n\n\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, "templateScope": "local", "className": "", - "x": 870, - "y": 840, + "x": 1430, + "y": 320, + "wires": [ + [] + ] + }, + { + "id": "d3a674f29c0cf4d5", + "type": "inject", + "z": "8bbc764cd051ef1b", + "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": [ [ - "3d0401ed2bb481cd" + "323e1bf353cf9505" ] ] }, + { + "id": "323e1bf353cf9505", + "type": "function", + "z": "8bbc764cd051ef1b", + "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": [ + [ + "c8c9a939c233c90f" + ] + ] + }, + { + "id": "c8c9a939c233c90f", + "type": "function", + "z": "8bbc764cd051ef1b", + "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": [ + [ + "f3d33e19798b479f", + "366f5e1cbb7a0f46" + ] + ] + }, + { + "id": "3c81e7f06c408f8a", + "type": "rbe", + "z": "8bbc764cd051ef1b", + "g": "a46fbefc8db2270b", + "name": "Only on change per channel", + "func": "rbe", + "gap": "", + "start": "", + "inout": "out", + "septopics": true, + "property": "payload.state", + "topi": "topic", + "x": 1700, + "y": 480, + "wires": [ + [ + "af2d1ab8e73b9667" + ] + ] + }, + { + "id": "d4bb5ead71493ebe", + "type": "udp out", + "z": "8bbc764cd051ef1b", + "g": "a46fbefc8db2270b", + "name": "OSC → CueCore2 10.2.71.130:8000", + "addr": "10.2.71.130", + "iface": "", + "port": "8000", + "ipv": "udp4", + "outport": "", + "base64": false, + "x": 2370, + "y": 480, + "wires": [] + }, + { + "id": "af2d1ab8e73b9667", + "type": "function", + "z": "8bbc764cd051ef1b", + "g": "a46fbefc8db2270b", + "name": "Encode OSC /position/ int ", + "func": "/**\n * One output (Buffer) → your UDP OSC node\n * Behavior:\n * - On state===1: send /position/,1 and mark idx as ON\n * - On state===0: unmark idx; if none left ON, send OFF (/position/0,0)\n */\n\n// ---------- CONFIG (change if your CueCore expects different OFF) ----------\nconst ON_ADDR = (i) => `/position/${i}`;\nconst ON_ARG = 1;\nconst OFF_ADDR = '/position/0';\nconst OFF_ARG = 0;\n\n// ---------- OSC helpers ----------\nfunction pad4(buf){ const pad=(4-(buf.length%4))%4; return Buffer.concat([buf, Buffer.alloc(pad)]); }\nfunction oscString(s){ return pad4(Buffer.concat([Buffer.from(s,'utf8'), Buffer.from([0]) ])); }\nfunction oscInt(i){ const b=Buffer.alloc(4); b.writeInt32BE(i,0); return b; }\nfunction makeOsc(addr, intVal){\n const a = oscString(addr);\n const t = oscString(',i');\n const v = oscInt(intVal);\n return Buffer.concat([a,t,v]);\n}\n\n// ---------- Input ----------\nconst { idx, state } = msg.payload || {};\nif (!idx || Number.isNaN(Number(idx))) return null;\n\n// ---------- Track which channels are ON ----------\nlet onSet = context.get('onSet') || {}; // { [idx]: true }\nlet changed = false;\n\nif (state === 1) {\n if (!onSet[idx]) {\n onSet[idx] = true;\n changed = true;\n }\n context.set('onSet', onSet);\n\n // Always point to the (new) ON speaker\n const buf = makeOsc(ON_ADDR(idx), ON_ARG);\n node.status({ text: `ON → /position/${idx}` });\n return { payload: buf };\n}\n\nif (state === 0) {\n if (onSet[idx]) {\n delete onSet[idx];\n changed = true;\n }\n context.set('onSet', onSet);\n\n // If no channels remain ON → send OFF\n if (Object.keys(onSet).length === 0) {\n const buf = makeOsc(OFF_ADDR, OFF_ARG);\n node.status({ text: 'ALL OFF → /position/0' });\n return { payload: buf };\n }\n\n // Some other channel still ON → no OSC needed\n node.status({ text: `OFF ${idx} (others still ON)` });\n return null;\n}\n\nreturn null;\n", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2000, + "y": 480, + "wires": [ + [ + "d4bb5ead71493ebe" + ] + ] + }, + { + "id": "f3d33e19798b479f", + "type": "debug", + "z": "8bbc764cd051ef1b", + "name": "FR1 raw", + "active": false, + "tosidebar": true, + "complete": "payload", + "x": 820, + "y": 520, + "wires": [] + }, + { + "id": "4e52ab5c6cc5f16a", + "type": "function", + "z": "8bbc764cd051ef1b", + "g": "a46fbefc8db2270b", + "name": "Parse CHxOUT=TRUE/0 → {idx,state}", + "func": "const s = String(msg.payload ?? '').trim();\n\n\n// SHould accept $CH12OUT, #CH_FROUT and #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\n// I need to get the leading digits after the #CH, exceptions are _FR and _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": 1290, + "y": 480, + "wires": [ + [ + "3c81e7f06c408f8a", + "91810af23bcdb612" + ] + ] + }, + { + "id": "91810af23bcdb612", + "type": "function", + "z": "8bbc764cd051ef1b", + "g": "66a230ec07338045", + "name": "function 2", + "func": "// Converts {idx, state} → UI message for uibuilder\nconst { idx, state } = msg.payload || {};\nif (!idx || Number.isNaN(idx)) return null;\n\n// Speaker names/descriptions (optional)\nconst SPEAKERS = {\n 6: { name: \"Front Left\", desc: \"Xilica AMP X — 500 W, 90°×40°\" },\n 5: { name: \"Front Right\", desc: \"Xilica AMP X — 500 W, 90°×40°\" },\n 7: { name: \"Front Right\", desc: \"Xilica AMP X — 500 W, 90°×40°\" },\n 19: { name: \"Balcony Left\", desc: \"Passive, 300 W, 100°×50°\" },\n 20: { name: \"Balcony Right\", desc: \"Passive, 300 W, 100°×50°\" },\n};\n\n// Remember which one is active\nlet active = context.get('active') || null;\n\nif (state === 1) {\n const spk = SPEAKERS[idx] || { name: `Speaker #${idx}`, desc: \"\" };\n context.set('active', idx);\n\n return {\n payload: {\n type: \"deviceInfo\",\n img: `./img/${idx}.png`, // uses your PNG files\n html: `\n

${spk.name}

\n

${spk.desc}

\n

Channel ${idx}

\n `\n }\n };\n}\n\nif (state === 0 && active === idx) {\n context.set('active', null);\n return { payload: { type: \"idle\" } };\n}\n\nreturn null;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1640, + "y": 580, + "wires": [ + [ + "a7f24e2687a40804" + ] + ] + }, + { + "id": "a7f24e2687a40804", + "type": "uibuilder", + "z": "8bbc764cd051ef1b", + "g": "66a230ec07338045", + "name": "DEVICE INFO PAGE", + "topic": "", + "url": "device-info", + "okToGo": true, + "fwdInMessages": false, + "allowScripts": false, + "allowStyles": false, + "copyIndex": true, + "templateFolder": "blank", + "extTemplate": "", + "showfolder": false, + "reload": false, + "sourceFolder": "src", + "deployedVersion": "7.5.0", + "showMsgUib": false, + "title": "", + "descr": "", + "editurl": "vscode://vscode-remote/ssh-remote+192.168.1.37/home/lavadmin/.node-red/uibuilder/device-info/?windowId=_blank", + "x": 1900, + "y": 580, + "wires": [ + [], + [] + ] + }, { "id": "ui_group_master", "type": "ui-group", "name": "Master Gain", "page": "ui_page_solaro", - "width": "2", + "width": "5", "height": "8", - "showTitle": true, - "className": "", - "visible": "true", - "disabled": "false", - "groupType": "default" - }, - { - "id": "ui_group_meters", - "type": "ui-group", - "name": "Meters", - "page": "ui_page_solaro", - "width": "6", - "height": "6", + "order": 1, "showTitle": true, "className": "", "visible": "true", @@ -364,6 +589,7 @@ ii[ "cols": "12" } ], + "order": 1, "className": "", "visible": "true", "disabled": "false" @@ -373,6 +599,11 @@ ii[ "type": "ui-base", "name": "Main UI", "path": "/ui", + "includeClientData": true, + "acceptsClientConfig": [ + "ui-control", + "ui-notification" + ], "headerContent": "page", "titleBarStyle": "default", "showReconnectNotification": true,