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); };