[ { "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": 268 }, { "id": "66a230ec07338045", "type": "group", "z": "8bbc764cd051ef1b", "g": "a46fbefc8db2270b", "name": "CueCore 2 UI (device info)", "style": { "label": true }, "nodes": [ "a7f24e2687a40804", "91810af23bcdb612", "07ea11aa3a92234b" ], "x": 1554, "y": 539, "w": 572, "h": 142 }, { "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" } }, { "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": "5", "height": "8", "order": 1, "showTitle": true, "className": "", "visible": "true", "disabled": "false", "groupType": "default" }, { "id": "0309a6afbb4232a9", "type": "global-config", "env": [] }, { "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 ", "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": 240, "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": 240, "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": [ [ "2141a20e5c74aa81", "7f1ff26625a38e58", "4e52ab5c6cc5f16a", "c8fc4af3e7fdbb59" ] ] }, { "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": "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": "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": "260885bbfa09d5c8", "type": "ui-template", "z": "8bbc764cd051ef1b", "group": "ui_group_master", "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": 320, "wires": [ [] ] }, { "id": "323e1bf353cf9505", "type": "function", "z": "8bbc764cd051ef1b", "name": "Round-robin (SUBSCRIBER)", "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\n/*\n* SUBSCRIPTIONS\n*/\n\n// Generate all channel names dynamically (CH4OUT → CH28OUT)\nfor (let i = 4; i <= 24; 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\nlines.push('SUBSCRIBE VU_MASTER \"TCP\"');\nlines.push('SUBSCRIBE MASTER_GAIN \"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": 640, "y": 400, "wires": [ [ "366f5e1cbb7a0f46", "83d5fbc8960e2b43" ] ] }, { "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": 1660, "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": "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": "UI mess convert", "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}.webp`, // 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": 1660, "y": 580, "wires": [ [ "a7f24e2687a40804", "07ea11aa3a92234b" ] ] }, { "id": "c8fc4af3e7fdbb59", "type": "debug", "z": "8bbc764cd051ef1b", "name": "Raw replies", "active": false, "tosidebar": true, "complete": "payload", "statusVal": "", "statusType": "auto", "x": 1170, "y": 400, "wires": [] }, { "id": "83d5fbc8960e2b43", "type": "debug", "z": "8bbc764cd051ef1b", "name": "Raw replies", "active": false, "tosidebar": true, "complete": "payload", "statusVal": "", "statusType": "auto", "x": 910, "y": 280, "wires": [] }, { "id": "7a3c4e3c8b5c0c8b", "type": "debug", "z": "8bbc764cd051ef1b", "name": "Raw replies", "active": true, "tosidebar": true, "complete": "payload", "statusVal": "", "statusType": "auto", "x": 990, "y": 500, "wires": [] }, { "id": "5f160e68439d75a7", "type": "function", "z": "8bbc764cd051ef1b", "name": "starter", "func": "// Load persisted flags\nlet xilicaLivePrev = context.get('xilicaLive') || false;\nlet visualProductionsLivePrev = context.get('visualProductionsLive') || false;\nlet systemOnPrev = context.get('systemOn') || false;\n\n// Update flags from this message\nlet xilicaLive = xilicaLivePrev;\nlet visualProductionsLive = visualProductionsLivePrev;\n\nif (msg.payload === 'XILICA=LIVE') xilicaLive = true;\nif (msg.payload === 'XILICA=DEAD') xilicaLive = false;\nif (msg.payload === 'VISUALPRODUCTIONS=LIVE') visualProductionsLive = true;\nif (msg.payload === 'VISUALPRODUCTIONS=DEAD') visualProductionsLive = false;\n\n// Persist device flags\ncontext.set('xilicaLive', xilicaLive);\ncontext.set('visualProductionsLive', visualProductionsLive);\n\n// Determine overall system state\nconst systemOnNow = xilicaLive && visualProductionsLive;\n\n// Detect device reconnect (Xilica DEAD→LIVE)\nconst xilicaReconnected = (!xilicaLivePrev && xilicaLive);\n\n// Detect overall ON/OFF change\nconst systemStateChanged = (systemOnNow !== systemOnPrev);\n\n// Persist new system state\ncontext.set('systemOn', systemOnNow);\n\n// Conditions that should trigger a re-subscribe:\nif (systemStateChanged || xilicaReconnected) {\n node.status({text:`Trigger subscribe (x:${xilicaLive} v:${visualProductionsLive} on:${systemOnNow})`});\n return { payload: 'SUBSCRIBE' };\n}\n\n// Otherwise do nothing\nreturn null;\n", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 830, "y": 720, "wires": [ [] ] }, { "id": "3eeee62903e4dc53", "type": "inject", "z": "8bbc764cd051ef1b", "name": "INJECT PING", "props": [ { "p": "payload" } ], "repeat": "2", "crontab": "", "once": true, "onceDelay": "0.5", "topic": "", "payload": "", "payloadType": "str", "x": 180, "y": 700, "wires": [ [ "2be25aa44261aba0", "4a69e31ccbd2b33a" ] ] }, { "id": "2be25aa44261aba0", "type": "ping", "z": "8bbc764cd051ef1b", "protocol": "Automatic", "mode": "triggered", "name": "", "host": "192.168.1.244", "timer": "20", "inputs": 1, "x": 390, "y": 680, "wires": [ [ "79313575c65917d2" ] ] }, { "id": "4a69e31ccbd2b33a", "type": "ping", "z": "8bbc764cd051ef1b", "protocol": "Automatic", "mode": "triggered", "name": "", "host": "10.2.71.130", "timer": "20", "inputs": 1, "x": 400, "y": 740, "wires": [ [ "bbae029ed37eba59" ] ] }, { "id": "79313575c65917d2", "type": "function", "z": "8bbc764cd051ef1b", "name": "xilica live?", "func": "const v = Number(msg.payload);\n// show value on the slider label while sending\nif(msg.payload != null){\n msg.payload = 'XILICA=LIVE';\n} else {\n msg.payload = 'XILICA=DEAD'\n}\nreturn { payload: msg.payload };\n", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 650, "y": 680, "wires": [ [ "5f160e68439d75a7" ] ] }, { "id": "bbae029ed37eba59", "type": "function", "z": "8bbc764cd051ef1b", "name": "visual productions live?", "func": "if(msg.payload != null){\n msg.payload = 'VISUALPRODUCTIONS=LIVE';\n} else {\n msg.payload = 'VISUALPRODUCTIONS=DEAD'\n}\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 610, "y": 740, "wires": [ [ "5f160e68439d75a7" ] ] }, { "id": "49cd35801588c2a9", "type": "inject", "z": "8bbc764cd051ef1b", "name": "INJECT PING", "props": [ { "p": "payload" } ], "repeat": "60", "crontab": "", "once": true, "onceDelay": "0.5", "topic": "", "payload": "", "payloadType": "str", "x": 360, "y": 380, "wires": [ [ "323e1bf353cf9505" ] ] }, { "id": "07ea11aa3a92234b", "type": "debug", "z": "8bbc764cd051ef1b", "g": "66a230ec07338045", "name": "Raw replies", "active": false, "tosidebar": true, "complete": "payload", "statusVal": "", "statusType": "auto", "x": 1890, "y": 640, "wires": [] }, { "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": 1960, "y": 580, "wires": [ [], [] ] }, { "id": "05ed1a581484a8f0", "type": "ui-slider", "z": "8bbc764cd051ef1b", "group": "ui_group_master", "name": "Dimmer", "label": "Dimmer", "tooltip": "", "order": 0, "width": 0, "height": 0, "passthru": false, "outs": "all", "topic": "topic", "topicType": "msg", "thumbLabel": "true", "showTicks": "always", "min": 0, "max": "100", "step": 1, "className": "", "iconPrepend": "", "iconAppend": "", "color": "", "colorTrack": "", "colorThumb": "", "showTextField": false, "x": 1940, "y": 340, "wires": [ [ "1f590b0ab6481968" ] ] }, { "id": "1f590b0ab6481968", "type": "function", "z": "8bbc764cd051ef1b", "name": "Dimmer OSC", "func": "/**\n * Input: msg.payload = 0..100 (percent from slider)\n * Output: msg.payload = Buffer (OSC packet)\n * Address: /core/va/1/set (int32 0..255)\n */\n\nconst pct = Number(msg.payload);\nif (!Number.isFinite(pct)) return null;\n\n// clamp 0..100 and convert to 0..255\nconst p = Math.max(0, Math.min(100, pct));\nconst val = Math.round(p * 255 / 100);\n\n// ---------- OSC helpers (same style as your /position encoder) ----------\nfunction pad4(buf) {\n const pad = (4 - (buf.length % 4)) % 4;\n return Buffer.concat([buf, Buffer.alloc(pad)]);\n}\n\nfunction oscString(s) {\n return pad4(Buffer.concat([Buffer.from(s, 'utf8'), Buffer.from([0])]));\n}\n\nfunction oscInt(i) {\n const b = Buffer.alloc(4);\n b.writeInt32BE(i, 0);\n return b;\n}\n\nfunction makeOsc(addr, intVal) {\n const a = oscString(addr);\n const t = oscString(',i'); // int argument\n const v = oscInt(intVal);\n return Buffer.concat([a, t, v]);\n}\n\n// build packet for Variable 1\nconst buf = makeOsc('/core/va/1/set', val);\nnode.status({ text: `Dimmer VA1 = ${val} (${p.toFixed(0)}%)` });\n\nmsg.payload = buf;\nreturn msg;\n", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 2150, "y": 340, "wires": [ [ "d4bb5ead71493ebe" ] ] }, { "id": "73778ab3a60941fe", "type": "ui-slider", "z": "8bbc764cd051ef1b", "group": "ui_group_master", "name": "Tilt", "label": "Tilt", "tooltip": "", "order": 0, "width": 0, "height": 0, "passthru": false, "outs": "all", "topic": "topic", "topicType": "msg", "thumbLabel": "true", "showTicks": "always", "min": 0, "max": "100", "step": 1, "className": "", "iconPrepend": "", "iconAppend": "", "color": "", "colorTrack": "", "colorThumb": "", "showTextField": false, "x": 1910, "y": 260, "wires": [ [ "e862c513a1798795" ] ] }, { "id": "e862c513a1798795", "type": "function", "z": "8bbc764cd051ef1b", "name": "Tilt OSC", "func": "/**\n * Input: msg.payload = 0..100 (percent from slider)\n * Output: msg.payload = Buffer (OSC packet)\n * OSC Address: /core/va/2/set (int32 0..255) -> Tilt variable\n */\n\nconst pct = Number(msg.payload);\nif (!Number.isFinite(pct)) return null;\n\n// clamp 0..100 and convert to 0..255\nconst p = Math.max(0, Math.min(100, pct));\nconst val = Math.round(p * 255 / 100);\n\n// ---------- OSC helpers ----------\nfunction pad4(buf) {\n const pad = (4 - (buf.length % 4)) % 4;\n return Buffer.concat([buf, Buffer.alloc(pad)]);\n}\n\nfunction oscString(s) {\n return pad4(Buffer.concat([Buffer.from(s, 'utf8'), Buffer.from([0])]));\n}\n\nfunction oscInt(i) {\n const b = Buffer.alloc(4);\n b.writeInt32BE(i, 0);\n return b;\n}\n\nfunction makeOsc(addr, intVal) {\n const a = oscString(addr);\n const t = oscString(',i'); // int argument\n const v = oscInt(intVal);\n return Buffer.concat([a, t, v]);\n}\n\n// build packet for Variable 2 (Tilt)\nconst buf = makeOsc('/core/va/2/set', val);\nnode.status({ text: `Tilt VA2 = ${val} (${p.toFixed(0)}%)` });\n\nmsg.payload = buf;\nreturn msg;\n", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 2120, "y": 260, "wires": [ [ "d4bb5ead71493ebe" ] ] }, { "id": "bb45bbb615881c63", "type": "ui-slider", "z": "8bbc764cd051ef1b", "group": "ui_group_master", "name": "Pan", "label": "Pan", "tooltip": "", "order": 0, "width": 0, "height": 0, "passthru": false, "outs": "all", "topic": "topic", "topicType": "msg", "thumbLabel": "true", "showTicks": "always", "min": 0, "max": "100", "step": 1, "className": "", "iconPrepend": "", "iconAppend": "", "color": "", "colorTrack": "", "colorThumb": "", "showTextField": false, "x": 1910, "y": 200, "wires": [ [ "0207c951094f5e4c" ] ] }, { "id": "0207c951094f5e4c", "type": "function", "z": "8bbc764cd051ef1b", "name": "Pan OSC", "func": "/**\n * Input: msg.payload = 0..100 (percent from slider)\n * Output: msg.payload = Buffer (OSC packet)\n * OSC Address: /core/va/2/set (int32 0..255) -> Tilt variable\n */\n\nconst pct = Number(msg.payload);\nif (!Number.isFinite(pct)) return null;\n\n// clamp 0..100 and convert to 0..255\nconst p = Math.max(0, Math.min(100, pct));\nconst val = Math.round(p * 255 / 100);\n\n// ---------- OSC helpers ----------\nfunction pad4(buf) {\n const pad = (4 - (buf.length % 4)) % 4;\n return Buffer.concat([buf, Buffer.alloc(pad)]);\n}\n\nfunction oscString(s) {\n return pad4(Buffer.concat([Buffer.from(s, 'utf8'), Buffer.from([0])]));\n}\n\nfunction oscInt(i) {\n const b = Buffer.alloc(4);\n b.writeInt32BE(i, 0);\n return b;\n}\n\nfunction makeOsc(addr, intVal) {\n const a = oscString(addr);\n const t = oscString(',i'); // int argument\n const v = oscInt(intVal);\n return Buffer.concat([a, t, v]);\n}\n\n// build packet for Variable 2 (Tilt)\nconst buf = makeOsc('/core/va/3/set', val);\nnode.status({ text: `Pan VA3 = ${val} (${p.toFixed(0)}%)` });\n\nmsg.payload = buf;\nreturn msg;\n", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 2120, "y": 200, "wires": [ [ "d4bb5ead71493ebe" ] ] }, { "id": "045c2998caddfbb0", "type": "ui-slider", "z": "8bbc764cd051ef1b", "group": "ui_group_master", "name": "Focus", "label": "Focus", "tooltip": "", "order": 0, "width": 0, "height": 0, "passthru": false, "outs": "all", "topic": "topic", "topicType": "msg", "thumbLabel": "true", "showTicks": "always", "min": 0, "max": "100", "step": 1, "className": "", "iconPrepend": "", "iconAppend": "", "color": "", "colorTrack": "", "colorThumb": "", "showTextField": false, "x": 1870, "y": 140, "wires": [ [ "f2c869993a5774cd" ] ] }, { "id": "f2c869993a5774cd", "type": "function", "z": "8bbc764cd051ef1b", "name": "Focus OSC", "func": "/**\n * Input: msg.payload = 0..100 (percent from slider)\n * Output: msg.payload = Buffer (OSC packet)\n * OSC Address: /core/va/2/set (int32 0..255) -> Tilt variable\n */\n\nconst pct = Number(msg.payload);\nif (!Number.isFinite(pct)) return null;\n\n// clamp 0..100 and convert to 0..255\nconst p = Math.max(0, Math.min(100, pct));\nconst val = Math.round(p * 255 / 100);\n\n// ---------- OSC helpers ----------\nfunction pad4(buf) {\n const pad = (4 - (buf.length % 4)) % 4;\n return Buffer.concat([buf, Buffer.alloc(pad)]);\n}\n\nfunction oscString(s) {\n return pad4(Buffer.concat([Buffer.from(s, 'utf8'), Buffer.from([0])]));\n}\n\nfunction oscInt(i) {\n const b = Buffer.alloc(4);\n b.writeInt32BE(i, 0);\n return b;\n}\n\nfunction makeOsc(addr, intVal) {\n const a = oscString(addr);\n const t = oscString(',i'); // int argument\n const v = oscInt(intVal);\n return Buffer.concat([a, t, v]);\n}\n\n// build packet for Variable 2 (Tilt)\nconst buf = makeOsc('/core/va/4/set', val);\nnode.status({ text: `Focus VA4 = ${val} (${p.toFixed(0)}%)` });\n\nmsg.payload = buf;\nreturn msg;\n", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 2090, "y": 140, "wires": [ [ "d4bb5ead71493ebe" ] ] } ]