diff --git a/nodes/xilica-ping.html b/nodes/xilica-ping.html index 305c249..762dcc3 100644 --- a/nodes/xilica-ping.html +++ b/nodes/xilica-ping.html @@ -1,17 +1,139 @@ -
- - -
+ + + + + + + + + + + diff --git a/nodes/xilica-ping.js b/nodes/xilica-ping.js index 2b95359..3df5ceb 100644 --- a/nodes/xilica-ping.js +++ b/nodes/xilica-ping.js @@ -1,12 +1,267 @@ -module.exports = function(RED) { +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); msg.payload = "xilica-ping node is alive"; send(msg); - done(); + if (done) { + done(); + } }); } + 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); };