commit e62ca4c2fd9b2c2cb129857e94e4624ec2314348 Author: marockaspark Date: Wed Nov 19 15:21:35 2025 +0100 Create project diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c9c01a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.backup \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f4d9283 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +showroom_flow +============= + +### About + +This is your project's README.md file. It helps users understand what your +project does, how to use it and anything else they may need to know. \ No newline at end of file diff --git a/flows.json b/flows.json new file mode 100644 index 0000000..05d76f4 --- /dev/null +++ b/flows.json @@ -0,0 +1,926 @@ +[ + { + "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" + ] + ] + } +] \ No newline at end of file diff --git a/flows_cred.json b/flows_cred.json new file mode 100644 index 0000000..3c11dd1 --- /dev/null +++ b/flows_cred.json @@ -0,0 +1,3 @@ +{ + "$": "73ef3d454b4dc31f7c94c0095f9dc56a3s0=" +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..bc49d0f --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "showroom_flow", + "description": "A Node-RED Project", + "version": "0.0.1", + "dependencies": {}, + "node-red": { + "settings": { + "flowFile": "flows.json", + "credentialsFile": "flows_cred.json" + } + } +} \ No newline at end of file