Files
xilica-node-red-integration/flows/showroom_flow.json

634 lines
23 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[
{
"id": "8bbc764cd051ef1b",
"type": "tab",
"label": "Xilica FR1 Dashboard",
"disabled": false,
"info": ""
},
{
"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": "8bbc764cd051ef1b",
"group": "ui_group_master",
"order": 3,
"width": "1",
"height": "1",
"name": "Current (dB)",
"label": "Current (dB)",
"layout": "row-spread",
"style": false,
"font": "",
"fontSize": "",
"color": "#000000",
"wrapText": false,
"className": "",
"value": "payload",
"valueType": "msg",
"x": 1470,
"y": 200,
"wires": []
},
{
"id": "db4f60616c5bb903",
"type": "ui-slider",
"z": "8bbc764cd051ef1b",
"group": "ui_group_master",
"name": "MASTER_GAIN (dB)",
"label": "MASTER_GAIN (dB)",
"order": 2,
"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": 180,
"y": 240,
"wires": [
[
"ab91656d7341e4d7"
]
]
},
{
"id": "ab91656d7341e4d7",
"type": "function",
"z": "8bbc764cd051ef1b",
"name": "build SET MASTER_GAIN <v>",
"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": 430,
"y": 240,
"wires": [
[
"f08eb151bb70d817"
]
]
},
{
"id": "72a778b813cb0915",
"type": "inject",
"z": "8bbc764cd051ef1b",
"name": "poll GET",
"props": [
{
"p": "payload"
}
],
"repeat": "0.1",
"crontab": "",
"once": true,
"onceDelay": "0.5",
"topic": "",
"payload": "GET MASTER_GAIN",
"payloadType": "str",
"x": 500,
"y": 340,
"wires": [
[
"f08eb151bb70d817"
]
]
},
{
"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": "8bbc764cd051ef1b",
"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": [
[
"c8fc4af3e7fdbb59",
"2141a20e5c74aa81",
"7f1ff26625a38e58",
"4e52ab5c6cc5f16a"
]
]
},
{
"id": "2141a20e5c74aa81",
"type": "function",
"z": "8bbc764cd051ef1b",
"name": "parse GET reply → number",
"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,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1220,
"y": 240,
"wires": [
[
"e57cccd09cde445b"
],
[
"dd021e3fbc30688d"
]
]
},
{
"id": "c8fc4af3e7fdbb59",
"type": "debug",
"z": "8bbc764cd051ef1b",
"name": "Raw replies",
"active": false,
"tosidebar": true,
"complete": "payload",
"statusVal": "",
"statusType": "auto",
"x": 1170,
"y": 280,
"wires": []
},
{
"id": "dd021e3fbc30688d",
"type": "link out",
"z": "8bbc764cd051ef1b",
"name": "link out 2",
"mode": "link",
"links": [
"fb625579ecce6897"
],
"x": 1405,
"y": 280,
"wires": []
},
{
"id": "fb625579ecce6897",
"type": "link in",
"z": "8bbc764cd051ef1b",
"name": "link in 2",
"links": [
"dd021e3fbc30688d"
],
"x": 35,
"y": 240,
"wires": [
[
"db4f60616c5bb903"
]
]
},
{
"id": "33a36424df3bc62c",
"type": "inject",
"z": "8bbc764cd051ef1b",
"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": [
[
"ca98de1bab349c37"
]
]
},
{
"id": "7f1ff26625a38e58",
"type": "function",
"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]); \nreturn msg;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1220,
"y": 320,
"wires": [
[
"260885bbfa09d5c8"
]
]
},
{
"id": "ca98de1bab349c37",
"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": 320,
"y": 400,
"wires": [
[
"366f5e1cbb7a0f46"
]
]
},
{
"id": "260885bbfa09d5c8",
"type": "ui-template",
"z": "8bbc764cd051ef1b",
"group": "ui_group_master",
"page": "",
"ui": "",
"name": "vu_master",
"order": 1,
"width": "2",
"height": "8",
"head": "",
"format": "<template>\n <div class=\"vu-vertical\">\n <div class=\"vu-hdr\">\n <strong>VU_MASTER</strong>\n <span class=\"vu-readout\">{{ level.toFixed(1) }} dB</span>\n </div>\n\n <div class=\"vu-bar\">\n <div class=\"vu-fill\" :style=\"{ height: percent + '%' }\"></div>\n <div class=\"vu-peak\" :style=\"{ bottom: peakPercent + '%' }\"></div>\n <span class=\"vu-tick\" v-for=\"t in ticks\" :key=\"t\" :style=\"{ bottom: t + '%' }\"></span>\n </div>\n\n <div class=\"vu-ftr\">\n <span>{{ max }} dB</span>\n <span>3</span>\n <span>12</span>\n <span>{{ min }} dB</span>\n </div>\n </div>\n</template>\n\n<script>\nexport default {\n props: ['msg'],\n data: () => ({\n min: -60,\n max: 0,\n level: -60,\n peak: -60,\n decayDbPerSec: 6,\n _timer: null,\n ticks: [0, 80, 95, 100]\n }),\n computed: {\n percent () {\n const v = Math.min(this.max, Math.max(this.min, this.level));\n return ((v - this.min) / (this.max - this.min)) * 100;\n },\n peakPercent () {\n const v = Math.min(this.max, Math.max(this.min, this.peak));\n return ((v - this.min) / (this.max - this.min)) * 100;\n }\n },\n watch: {\n msg: {\n deep: true,\n immediate: true,\n handler (m) {\n if (!m) return;\n if (m.min !== undefined && Number.isFinite(+m.min)) this.min = +m.min;\n if (m.max !== undefined && Number.isFinite(+m.max)) this.max = +m.max;\n if (m.payload !== undefined) {\n const n = Number(m.payload);\n if (Number.isFinite(n)) {\n this.level = n;\n if (n > this.peak) this.peak = n;\n }\n }\n }\n }\n },\n mounted () {\n const dt = 100;\n this._timer = setInterval(() => {\n const step = this.decayDbPerSec * (dt / 1000);\n if (this.peak > this.level) this.peak = Math.max(this.level, this.peak - step);\n else this.peak = this.level;\n }, dt);\n },\n unmounted () {\n if (this._timer) clearInterval(this._timer);\n }\n}\n</script>\n\n<style>\n.vu-vertical {\n width: 100px;\n height: 450px;\n display: flex;\n flex-direction: column;\n align-items: center;\n font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n}\n\n.vu-hdr, .vu-ftr {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n}\n\n.vu-readout {\n font-variant-numeric: tabular-nums;\n opacity: .8;\n}\n\n.vu-bar {\n position: relative;\n flex: 1;\n width: 30px;\n margin: 6px 0;\n border-radius: 4px;\n background: #0b0f14;\n overflow: hidden;\n box-shadow: inset 0 0 0 1px rgba(255,255,255,.06);\n display: flex;\n flex-direction: column-reverse;\n}\n\n.vu-fill {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n z-index: 1;\n transition: height 90ms linear;\n background: linear-gradient(0deg,\n #10b981 0%, #10b981 80%, /* green up to -12 dB */\n #f59e0b 80%, #f59e0b 95%, /* amber to -3 dB */\n #ef4444 95%, #ef4444 100%); /* red above -3 dB */\n}\n\n.vu-peak {\n position: absolute;\n left: 0;\n right: 0;\n height: 2px;\n background: rgba(255,255,255,.9);\n box-shadow: 0 0 4px rgba(255,255,255,.9);\n z-index: 2;\n}\n\n.vu-tick {\n position: absolute;\n left: 0;\n right: 0;\n height: 1px;\n background: rgba(255,255,255,.15);\n z-index: 0;\n}\n\n.vu-ftr span {\n font-size: 12px;\n color: rgba(255,255,255,.6);\n}\n</style>\n",
"storeOutMessages": true,
"passthru": true,
"resendOnRefresh": true,
"templateScope": "local",
"className": "",
"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": [
[
"323e1bf353cf9505"
]
]
},
{
"id": "323e1bf353cf9505",
"type": "function",
"z": "8bbc764cd051ef1b",
"name": "Round-robin GET CH4/5/6",
"func": "// Build a single multi-line SUBSCRIBE message for CH4OUTCH28OUT\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/<idx> int <state>",
"func": "/**\n * One output (Buffer) → your UDP OSC node\n * Behavior:\n * - On state===1: send /position/<idx>,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 <h2>${spk.name}</h2>\n <p>${spk.desc}</p>\n <p><small><em>Channel ${idx}</em></small></p>\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": "5",
"height": "8",
"order": 1,
"showTitle": true,
"className": "",
"visible": "true",
"disabled": "false",
"groupType": "default"
},
{
"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_base_main",
"type": "ui-base",
"name": "Main UI",
"path": "/ui",
"includeClientData": true,
"acceptsClientConfig": [
"ui-control",
"ui-notification"
],
"headerContent": "page",
"titleBarStyle": "default",
"showReconnectNotification": true,
"notificationDisplayTime": 5,
"showDisconnectNotification": true,
"allowInstall": true
},
{
"id": "efd7cd1777464c3d",
"type": "ui-theme",
"name": "Theme Name",
"colors": {
"surface": "#ffffff",
"primary": "#0094ce",
"bgPage": "#eeeeee",
"groupBg": "#ffffff",
"groupOutline": "#cccccc"
},
"sizes": {
"density": "default",
"pagePadding": "12px",
"groupGap": "12px",
"groupBorderRadius": "4px",
"widgetGap": "12px"
}
}
]