From 427bfdfabf22af4ed500b19da55737ba83571c77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20Vuji=C4=87?= Date: Wed, 31 Dec 2025 08:28:11 +0100 Subject: [PATCH] command, connect and ping nodes split --- nodes/xilica-command.html | 66 +++++++++ nodes/xilica-command.js | 243 +++++++++++++++++++++++++++++++++ nodes/xilica-connection.html | 52 +++++++ nodes/xilica-connection.js | 16 +++ nodes/xilica-ping.html | 114 ---------------- nodes/xilica-ping.js | 255 +---------------------------------- package.json | 5 +- 7 files changed, 383 insertions(+), 368 deletions(-) create mode 100644 nodes/xilica-command.html create mode 100644 nodes/xilica-command.js create mode 100644 nodes/xilica-connection.html create mode 100644 nodes/xilica-connection.js diff --git a/nodes/xilica-command.html b/nodes/xilica-command.html new file mode 100644 index 0000000..5a5bd22 --- /dev/null +++ b/nodes/xilica-command.html @@ -0,0 +1,66 @@ + + + + + + diff --git a/nodes/xilica-command.js b/nodes/xilica-command.js new file mode 100644 index 0000000..c67b2cc --- /dev/null +++ b/nodes/xilica-command.js @@ -0,0 +1,243 @@ +const net = require("net"); + +module.exports = function (RED) { + function parseResponseLines(lines) { + const results = []; + lines.forEach((line) => { + if (!line) { + return; + } + if (line === "OK") { + results.push({ type: "ok" }); + return; + } + if (line.startsWith("ERROR=")) { + const code = parseInt(line.substring(6), 10); + results.push({ type: "error", code, raw: line }); + return; + } + if (line[0] === "#") { + const payload = line.slice(1); + const idx = payload.indexOf("="); + if (idx !== -1) { + const control = payload.slice(0, idx); + const data = payload.slice(idx + 1); + results.push({ + type: "notification", + control, + data, + }); + return; + } + } + const idx = line.indexOf("="); + if (idx !== -1) { + const control = line.slice(0, idx); + const data = line.slice(idx + 1); + results.push({ + type: "value", + control, + data, + }); + return; + } + results.push({ type: "raw", raw: line }); + }); + + if (results.length === 1) { + return results[0]; + } + return results; + } + + function XilicaCommand(config) { + RED.nodes.createNode(this, config); + const node = this; + + node.connectionConfig = RED.nodes.getNode(config.connection); + node.command = config.command || ""; + node.control = config.control || ""; + node.data = config.data || ""; + node.parseResponse = config.parseResponse !== false; + + if (!node.connectionConfig) { + node.status({ fill: "red", shape: "ring", text: "no connection" }); + } + + node.on("input", (msg, send, done) => { + send = + send || + function () { + node.send.apply(node, arguments); + }; + done = done || function () {}; + + const conn = node.connectionConfig; + if (!conn || !conn.host) { + node.status({ fill: "red", shape: "ring", text: "no host" }); + node.error("No Xilica connection configured", msg); + done(); + return; + } + + let rawCommand = ""; + if (typeof msg.payload === "string" && msg.payload.trim()) { + rawCommand = msg.payload.trim(); + } + + let cmd = node.command; + let control = node.control; + let data = node.data; + + if (msg.command) { + cmd = msg.command; + } + if (msg.control) { + control = msg.control; + } + if (Object.prototype.hasOwnProperty.call(msg, "data")) { + data = msg.data; + } + + if (!rawCommand) { + if (!cmd) { + node.status({ fill: "red", shape: "ring", text: "missing command" }); + node.error("Missing command", msg); + done(); + return; + } + + rawCommand = String(cmd).trim(); + if (control) { + rawCommand += " " + String(control).trim(); + } + if (data !== undefined && data !== "") { + rawCommand += " " + String(data).trim(); + } + } + + const host = conn.host; + const port = conn.port || 10007; + const timeout = conn.timeout || 5000; + const password = + conn.credentials && conn.credentials.password + ? conn.credentials.password + : null; + + const client = net.createConnection({ host, port }); + let buffer = ""; + let phase = password ? "login" : "command"; + const responseLines = []; + let finished = false; + let idleTimer = null; + const idleTimeout = 100; + + function clearIdle() { + if (idleTimer) { + clearTimeout(idleTimer); + idleTimer = null; + } + } + + function scheduleIdleFinish() { + clearIdle(); + idleTimer = setTimeout(() => { + finishSuccess(); + }, idleTimeout); + } + + function finishSuccess() { + if (finished) { + return; + } + finished = true; + clearIdle(); + client.end(); + node.status({}); + + msg.raw = responseLines.slice(); + if (node.parseResponse) { + msg.payload = parseResponseLines(responseLines); + } else { + msg.payload = responseLines.join("\n"); + } + + send(msg); + done(); + } + + function finishError(err) { + if (finished) { + return; + } + finished = true; + clearIdle(); + client.destroy(); + node.status({ fill: "red", shape: "ring", text: "error" }); + node.error(err, msg); + done(err); + } + + client.setTimeout(timeout); + + client.on("connect", () => { + node.status({ fill: "green", shape: "dot", text: "connected" }); + try { + if (phase === "login") { + client.write("LOGIN " + password + "\r"); + } else { + client.write(rawCommand + "\r"); + } + } catch (err) { + finishError(err); + } + }); + + client.on("data", (dataBuf) => { + buffer += dataBuf.toString("utf8"); + let idx = buffer.indexOf("\r"); + while (idx !== -1) { + const line = buffer.slice(0, idx); + buffer = buffer.slice(idx + 1); + + if (phase === "login") { + if (line && line.startsWith("ERROR=")) { + finishError(new Error("Login failed: " + line)); + return; + } + phase = "command"; + try { + client.write(rawCommand + "\r"); + } catch (err) { + finishError(err); + } + } else { + if (line) { + responseLines.push(line); + } + scheduleIdleFinish(); + } + + idx = buffer.indexOf("\r"); + } + }); + + client.on("timeout", () => { + finishError(new Error("Timeout waiting for response from Xilica")); + }); + + client.on("error", (err) => { + finishError(err); + }); + + client.on("end", () => { + if (!finished && responseLines.length > 0) { + finishSuccess(); + } + }); + }); + } + + RED.nodes.registerType("xilica-command", XilicaCommand); +}; + diff --git a/nodes/xilica-connection.html b/nodes/xilica-connection.html new file mode 100644 index 0000000..dcf4469 --- /dev/null +++ b/nodes/xilica-connection.html @@ -0,0 +1,52 @@ + + + + + + diff --git a/nodes/xilica-connection.js b/nodes/xilica-connection.js new file mode 100644 index 0000000..26fd0bb --- /dev/null +++ b/nodes/xilica-connection.js @@ -0,0 +1,16 @@ +module.exports = function (RED) { + function XilicaConnection(config) { + RED.nodes.createNode(this, config); + this.host = config.host; + this.port = parseInt(config.port, 10) || 10007; + this.timeout = parseInt(config.timeout, 10) || 5000; + this.credentials = this.credentials || {}; + } + + RED.nodes.registerType("xilica-connection", XilicaConnection, { + credentials: { + password: { type: "password" }, + }, + }); +}; + diff --git a/nodes/xilica-ping.html b/nodes/xilica-ping.html index 762dcc3..38aef85 100644 --- a/nodes/xilica-ping.html +++ b/nodes/xilica-ping.html @@ -10,53 +10,6 @@ RED.nodes.registerType('xilica-ping', { icon: "font-awesome/fa-plug", label: function () { return this.name || "xilica ping"; } }); - -RED.nodes.registerType('xilica-connection', { - category: 'config', - defaults: { - name: { value: "" }, - host: { value: "", required: true }, - port: { value: 10007, required: true }, - timeout: { value: 5000 } - }, - credentials: { - password: { type: "password" } - }, - label: function () { - if (this.name) { - return this.name; - } - if (this.host && this.port) { - return this.host + ":" + this.port; - } - return "Xilica connection"; - } -}); - -RED.nodes.registerType('xilica-command', { - category: 'Xilica', - color: '#D8E7FF', - defaults: { - name: { value: "" }, - connection: { value: "", type: "xilica-connection", required: true }, - command: { value: "" }, - control: { value: "" }, - data: { value: "" }, - parseResponse: { value: true } - }, - inputs: 1, - outputs: 1, - icon: "font-awesome/fa-plug", - label: function () { - if (this.name) { - return this.name; - } - if (this.command) { - return "xilica " + this.command.toLowerCase(); - } - return "xilica command"; - } -}); - - - - - - - - diff --git a/nodes/xilica-ping.js b/nodes/xilica-ping.js index 3df5ceb..aed8443 100644 --- a/nodes/xilica-ping.js +++ b/nodes/xilica-ping.js @@ -1,10 +1,10 @@ -const net = require("net"); - module.exports = function (RED) { function XilicaPing(config) { RED.nodes.createNode(this, config); this.on("input", (msg, send, done) => { - send = send || function () { this.send(msg); }.bind(this); + send = send || function () { + this.send(msg); + }.bind(this); msg.payload = "xilica-ping node is alive"; send(msg); if (done) { @@ -14,254 +14,5 @@ module.exports = function (RED) { } RED.nodes.registerType("xilica-ping", XilicaPing); - - function XilicaConnection(config) { - RED.nodes.createNode(this, config); - this.host = config.host; - this.port = parseInt(config.port, 10) || 10007; - this.timeout = parseInt(config.timeout, 10) || 5000; - this.credentials = this.credentials || {}; - } - - RED.nodes.registerType("xilica-connection", XilicaConnection, { - credentials: { - password: { type: "password" }, - }, - }); - - function parseResponseLines(lines) { - const results = []; - lines.forEach((line) => { - if (!line) { - return; - } - if (line === "OK") { - results.push({ type: "ok" }); - return; - } - if (line.startsWith("ERROR=")) { - const code = parseInt(line.substring(6), 10); - results.push({ type: "error", code, raw: line }); - return; - } - if (line[0] === "#") { - const payload = line.slice(1); - const idx = payload.indexOf("="); - if (idx !== -1) { - const control = payload.slice(0, idx); - const data = payload.slice(idx + 1); - results.push({ - type: "notification", - control, - data, - }); - return; - } - } - const idx = line.indexOf("="); - if (idx !== -1) { - const control = line.slice(0, idx); - const data = line.slice(idx + 1); - results.push({ - type: "value", - control, - data, - }); - return; - } - results.push({ type: "raw", raw: line }); - }); - - if (results.length === 1) { - return results[0]; - } - return results; - } - - function XilicaCommand(config) { - RED.nodes.createNode(this, config); - const node = this; - - node.connectionConfig = RED.nodes.getNode(config.connection); - node.command = config.command || ""; - node.control = config.control || ""; - node.data = config.data || ""; - node.parseResponse = config.parseResponse !== false; - - if (!node.connectionConfig) { - node.status({ fill: "red", shape: "ring", text: "no connection" }); - } - - node.on("input", (msg, send, done) => { - send = send || function () { node.send.apply(node, arguments); }; - done = done || function () {}; - - const conn = node.connectionConfig; - if (!conn || !conn.host) { - node.status({ fill: "red", shape: "ring", text: "no host" }); - node.error("No Xilica connection configured", msg); - done(); - return; - } - - let rawCommand = ""; - if (typeof msg.payload === "string" && msg.payload.trim()) { - rawCommand = msg.payload.trim(); - } - - let cmd = node.command; - let control = node.control; - let data = node.data; - - if (msg.command) { - cmd = msg.command; - } - if (msg.control) { - control = msg.control; - } - if (Object.prototype.hasOwnProperty.call(msg, "data")) { - data = msg.data; - } - - if (!rawCommand) { - if (!cmd) { - node.status({ fill: "red", shape: "ring", text: "missing command" }); - node.error("Missing command", msg); - done(); - return; - } - - rawCommand = String(cmd).trim(); - if (control) { - rawCommand += " " + String(control).trim(); - } - if (data !== undefined && data !== "") { - rawCommand += " " + String(data).trim(); - } - } - - const host = conn.host; - const port = conn.port || 10007; - const timeout = conn.timeout || 5000; - const password = - conn.credentials && conn.credentials.password - ? conn.credentials.password - : null; - - const client = net.createConnection({ host, port }); - let buffer = ""; - let phase = password ? "login" : "command"; - const responseLines = []; - let finished = false; - let idleTimer = null; - const idleTimeout = 100; - - function clearIdle() { - if (idleTimer) { - clearTimeout(idleTimer); - idleTimer = null; - } - } - - function scheduleIdleFinish() { - clearIdle(); - idleTimer = setTimeout(() => { - finishSuccess(); - }, idleTimeout); - } - - function finishSuccess() { - if (finished) { - return; - } - finished = true; - clearIdle(); - client.end(); - node.status({}); - - msg.raw = responseLines.slice(); - if (node.parseResponse) { - msg.payload = parseResponseLines(responseLines); - } else { - msg.payload = responseLines.join("\n"); - } - - send(msg); - done(); - } - - function finishError(err) { - if (finished) { - return; - } - finished = true; - clearIdle(); - client.destroy(); - node.status({ fill: "red", shape: "ring", text: "error" }); - node.error(err, msg); - done(err); - } - - client.setTimeout(timeout); - - client.on("connect", () => { - node.status({ fill: "green", shape: "dot", text: "connected" }); - try { - if (phase === "login") { - client.write("LOGIN " + password + "\r"); - } else { - client.write(rawCommand + "\r"); - } - } catch (err) { - finishError(err); - } - }); - - client.on("data", (dataBuf) => { - buffer += dataBuf.toString("utf8"); - let idx = buffer.indexOf("\r"); - while (idx !== -1) { - const line = buffer.slice(0, idx); - buffer = buffer.slice(idx + 1); - - if (phase === "login") { - if (line && line.startsWith("ERROR=")) { - finishError(new Error("Login failed: " + line)); - return; - } - phase = "command"; - try { - client.write(rawCommand + "\r"); - } catch (err) { - finishError(err); - } - } else { - if (line) { - responseLines.push(line); - } - scheduleIdleFinish(); - } - - idx = buffer.indexOf("\r"); - } - }); - - client.on("timeout", () => { - finishError(new Error("Timeout waiting for response from Xilica")); - }); - - client.on("error", (err) => { - finishError(err); - }); - - client.on("end", () => { - if (!finished && responseLines.length > 0) { - finishSuccess(); - } - }); - }); - } - - RED.nodes.registerType("xilica-command", XilicaCommand); }; diff --git a/package.json b/package.json index b1cd422..42af8b5 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,9 @@ "dependencies": {}, "node-red": { "nodes": { - "xilica-ping": "nodes/xilica-ping.js" + "xilica-ping": "nodes/xilica-ping.js", + "xilica-connection": "nodes/xilica-connection.js", + "xilica-command": "nodes/xilica-command.js" } } } -